Five usability rules of thumb on how to design your API's.
Bill Venners suggests that API design should consider programmers just like traditional users. It's a sentiment echoed elsewhere, Ken Arnold opines:
designing tools for programmers, including languages, APIs, and compilers, is a human factors problem
It's a huge leap to recognize this reality. Graham Glass, the creator of GLUE and founder of The Mind Electric (TME), blogs:
TME has a philosophy for designing software that I would like to share. The starting point for anything we build is the "user experience". Before we even think about stuff like standards compliance or implementation, we focus on what a user of our software/APIs will experience. Simplicity is the #1 objective. Only when this has been accomplished (at least on paper) do we talk about implementation details. Only when this has been simplified do we think about standards compliance. Even though this might sound backwards, it always seems to work out well in the end.
Developing webservices with GLUE is an extremely pleasant experience (you can see a viewlet of it in action here). Thus, simplicity concerns override all other concerns, it does remind me of the "worse is better" philosophy to language design.
We can learn alot by studying the API's of GLUE, however has anyone written any tips on how to do this? I do recall a couple. Ken Arnold says:
Any mistakes you can't make impossible, you want to catch. In other words, first design such that improper things can't even be written or expressed. They are impossible to do. Errors you can't make impossible, you want to catch right away. So as soon as a user makes a mistake he or she is told.
Don't give people a method that does something error-prone. Give them the method that allows them to do the subset that is not error-prone. Suppose you have a file object on which you can call open and close. You can only close an open file. You shouldn't be able to call close on the file object, because it may not be open. The open method should return something on which you can invoke close.
The first suggest seems to allude to the checked exception handling found in Java. At this moment there's plenty debate regarding that topic. The second seems to allude to the Curried Object pattern, that is encapsulate complex interactions in an object.
Kartzen Lentzsch, of JGoodies fame, writes about his reasoning behind his new Layout API:
SpringLayout needs more code than the FormLayout to express simple but frequently used form design. From my perspecitive, the main problem with SpringLayout is, that the layout specification language (Java code) doesn't represent the human mental layout model well. You can hardly seethe layout by just looking at the panel building code. FormLayout's layout specification language has been designed to express how many people think and talk about layout. How many lines of code do you need to build the form in Example 3 with the SpringLayout?
FormLayout favors an expressive layout specification over extensibility. It has been designed to be powerful and flexible enough to layout almost every panel that I've designed during the last decade. It covers all panels in the JGoodies tools and seems to be able to layout all panels in large apps like the Eclipse JDT and the NetBeans IDE. And so, I doubt that there's a need to extend the FormLayout, at least it isn't urgent.
The first insight recommends that API's be defined to reflect the mental model of?the programmer. The second is another suggestion, provide expressibility over extensibility.
That's four rules of thumb on how to design your API's, could there be more? I am sure that most programmers are completely unaware of these, in particular Sun's java programmers who continually create API's that favor complexity and extensibility over simplicity and expressibility. It's possibly a good exercise to collect all the "hall of shame" APIs we can find in the standard java libraries.
Always nice to see this topic discussed. I know that "programmers as users" is an idea that many are favorable to. But it's a notion that has no name, and so many people think they are alone or rare for thinking about it. One of my main hopes is to help bring the idea to the fore so that everyone who thinks about programmers as users can be out of the closet, so to speak.
When I said "Errors you can't make impossible, you want to catch right away," I wasn't just talking about checked vs. unchecked exceptions. That's just one way of thinking about the issue. The real issue is that you should check what you can check as soon as you can. For example, if you are taking information in to use later, validate it when you take it in, not when you use it. Jini's JoinManager requires an object that is serializable, but which will be serialized by an independent thread. The object should be serialized as a check when you give it to JoinManager, not have a serialization problem happen in the independent thread.
Throwing a checked exception might be a good response to detecting a failure (sometimes it is, sometimes it isn't). But check as soon as you can.
People do this naturally for (say) parameters that can't be negative. But they might consider it too expensive to serialize an object just to test if it is serializable. I say "Bah!" -- do it anyway. If you can, reuse that work for later (e.g., have the independent thread use the pre-serialized value if possible), but don't delay if at all possible.
Maybe "Check as early as possible", "Don't delay checking" or "Don't let errors propagate" may have been clearer for me. Yes, it still remains a good rule of thumb. The side of effect of NOT doing this is that information about the cause of an error is lost when an exception is eventually thrown. In otherwords its not in the stacktrace. Really a nasty thing to debug!
Small point regarding the idea of programmers-as-users/consumers: it seems to me that going the extra mile with javadoc comments, and editing it with some care, can go a long way to providing "excellent customer service". It does not seem to be widely used in this way, however...
Here are some ideas that I find useful: - use the constructor as a "back door", for passing in data that cannot be passed to a method argument because of signature constraints (this is used repeatedly in Design Patterns) - the caller is king; the caller determines both the name of the method and the return value; the args seem to be more a matter of cooperation between the class and its caller - if the caller passes in invalid args, throw a RuntimeException. Do not throw an assert. Follow the Sun guidelines and never use assert to check args to non-private methods. - items are package-private by default; if more scope is needed, add it in later when circumstances push you there - order dependencies are usually unnecessary; if needed, then minimize them with extreme prejudice - no mutable public data - javadoc all non-private items fully, completely - Pragmatic Programmers: "names are deeply meaningful to your brain". If you think of a better name, use it. - stick to familiar exception classes. Don't create your own. - prefer Collection over arrays. - passing in true/false literals makes a method call less intelligible at the point of call than it should be. Instead, define a simple inner class/enum which wraps these values, and force the caller to use that instead. - do not gratuitously implement Cloneable - ditto for Serializable - clarify if defensive copies are made or not - do not rely on finalize; instead provide an explicit cleanup method; this method can be called by finalize as a back up in case the caller forgets to call the explicit clean-up - avoid null, unless it is clearly the simplest answer - it is helpful if overloads differ in the number of args - value objects (VOs) should almost always implement equals and hashCode - equals and hashCode always go together - if you are an application programmer, and not building a framework meant to be subclassed, using final classes as the default is attractive, since it makes the program design and implementation simpler. - immutable value objects simplify the caller greatly
A lot of program construction is summarized by : 1) take a good guess 2) find the errors 3) fix the errors 4) repeat
For me, the sweet spot seems to be (2). "The best design techniques find the errors the fastest."
I think a little context-definition would be helpful at this point - I doubt if you guys are proposing Yet Another Universal Law of Programming. In particular, I am thinking about Tim Bray's infamous article "XML Is Too Hard": http://www.tbray.org/ongoing/When/200x/2003/03/16/XML-Prog where he categorizes the programming world into three communities: the Scripting Tribe, the OO Factory, and the Close-to-the-Metal Gang.
Now, your law of API design fit OO development well, but it's much less clear how applicable it's to the other two - where nimbleness trumps correctness as a concern. More specifically, flexibility (for scripters) and raw speed (for system coders).
Ken writes: Don't give people a method that does something error-prone. Give them the method that allows them to do the subset that is not error-prone. Suppose you have a file object on which you can call open and close. You can only close an open file. You shouldn't be able to call close on the file object, because it may not be open. The open method should return something on which you can invoke close.
I think the general notion of trying to prevent errors from being possible is a great one. However, I'm trying to understand the example given. At first it sounded very logical, but upon further thought it occurred to me that this solution simply defers the problem for the user doesn't it? What happens if the user calls close() on the object they received from open() twice? Obviously it helps them the first go 'round, but the second go 'round they are left with the same problem, aren't they? Only now they have to deal with 2 different class types, instead of just one. Just as they didn't know just by virtue of having a File object whether it was open or not, they don't know just by virtue of having a "FileOpen" object whether it is open or not.
It seems that with this example, there really isn't any way to *prevent* the error without changing the behavioral contract - namely by making it possible to call close() on a non-open file without an error. If this is not possible, it seems like a simpler design to clearly document the responsibility of the caller to either 1) keep track of the number of calls to open()/close(), and/or 2) provide an isOpen() method that a user can call prior to calling close(), and then clearly document the resulting exception if they don't follow the rules.