Usability and Class Library Design

Guidelines for simplicity and power

Arthur T. Jolin

Art designs and writes class libraries and frameworks for IBM. He has worked in languages from Fortran to Java for 19 years. You can reach him at jolin@raleigh.ibm.com.


As programmers are expected to do more and more development in less and less time, we can no longer afford to deal with colorful idiosyncrasies that interfere with getting the job done. The usability of class libraries and frameworks is being placed on an equal footing with their raw functionality and the speed of the resulting code.

Using general guidelines for user-interface usability as an outline, I've pulled together a set of rules for class library and framework usability. These guidelines have been established over time during actual class library and framework development. (Some have even been carried over from procedural development.) These guidelines are not about writing efficient code, although they don't make your code any less efficient. Nor are these guidelines about writing easy-to-maintain code, although they may provide that as a side effect. These guidelines are about making it easier for programmers to learn your class libraries or frameworks-and to write good programs with them.

Keep it Simple

When designing class libraries or frameworks, your primary goal should be to have as few classes, methods, and parameters as possible to achieve the function you are providing. The number of classes and methods relates neither to the quality of design nor necessarily to functionality. Bragging about large size is an admission of failure in design; developers are rightly balancing the appearance of rich function with the learning curve of a large library. If you have to brag, do so on what can be done with the classes.

However, you shouldn't write huge methods just to reduce the total. The flexibility benefits of small methods must be balanced against the cost of learning many methods. One way to reduce the number of methods is to keep some of them private. Better still, isolate and remove useless methods entirely.

Cognitive scientists discovered years ago that the human mind can typically deal with seven (plus or minus two) unrelated items (digits in a phone number, words in a random list, and so on) at a time. More complex data is best handled as hierarchies, where each level has this magic number of nodes. This cognitive ideal is sadly too much to ask of class libraries. However, you can keep things simpler by organizing classes into a number of smaller, relatively separate, class libraries. The IBM Open Class Library in VisualAge C++, for example, has one library for creating GUI-control-based interfaces, and another for drawing lines, circles, text, and the like.

While it must be as easy to work with libraries together as separately, it's important that users can learn them separately. This is only possible if libraries are truly separate. For example, libraries should not use each other's classes as parameters or return values except in those few places necessary to make them work together. In the case of Open Class, the connection point is that the "context" used by the drawing classes can be obtained from a window object in the GUI library. This is the only place the two class libraries refer to each other, but it is enough to allow integration.

In frameworks, a number of classes usually interact to accomplish a given task; this set of classes is called a "clique." You should minimize the number of classes in a given clique, especially the number users must derive from to accomplish the clique's task. For example, Liant's C++/Views class library has a menu clique consisting of only VMenu, VMenuItem, VPopupMenu, and method (see the guidelines on consistent capitalization), with VOrdCollect as an optional fifth class. This clique is even within the seven-plus-or-minus-two ideal.

Help Users be Productive Quickly

You might expect that helping users quickly become productive refers to installation and initial configuration (setup). However, other aspects come to mind if you think of "setup" in the more general sense.

For starters, you should minimize the classes that must be subclassed before they can be used. Every subclass is another class users must document (at least internally) and maintain (probably forever). Every additional subclass may even need to be defined in, and managed by, a code-control library (which must be accounted for in the compile-build process). It is easier for developers to find the class they want and instantiate it, than to pause, write a subclass, implement some method overrides, and go back to the original problem being solved.

For those classes that are intended to be subclassed, you should minimize the number of methods that must be overridden to accomplish the job. This does not mean that you want to forbid developers from overriding other methods in special situations, nor mash several functions together just to reduce the method count. The goal is to keep them to a minimum without violating the other usability guidelines.

Identify the Tools for a Task

It should be clear to users what items they need to perform a given task. Therefore, you should clearly identify those classes that are purely for your internal implementation and that you don't want to ship in your final product (unless you include source code). For example, include a comment such as //**NOSHIP*** INTERNAL USE ONLY in C++ headers and document what this comment means. In this way, if you find yourself accidentally exposing one of these headers, the chance of catching the error prior to shipment is much better.

You should also clearly identify the classes that must be subclassed to do a particular task, and the methods within those classes that must be overridden. A number of class libraries, including Microsoft's MFC, Borland's OWL, and IBM's Open Class, have identified certain methods as "advanced," or "implementation," or some other magic word indicating to the developer that this method is not used in the typical situation (but may be for more unusual ones). It appears unavoidable that there be such methods; this is especially true in frameworks, where, by definition, your classes interact in semiprivate ways. How you will inform the developer should be planned from the beginning.

Use Real-World Knowledge to Speed Understanding

The ability to transfer knowledge from the real world to a computer interface helps users instantly understand how to use (at least parts of) a system. This is equally true for programming interfaces. The main way knowledge transfer takes place here is in naming conventions for classes and methods.

Pick class and method names from the domain to which this library applies. If you are writing a financial-analysis class library, classes named StockPortfolio and MarginAccount are reasonable (not to mention obvious).

Less obvious are the guidelines involving natural-language syntax in naming. Generally, name classes with nouns and methods with verbs or verb phrases. The goal is to achieve a sort of object-action, "reverse English" sense out of the resulting source code, such as file->print() or aCollection sort. Smalltalk is famous for its elaborate pseudosentences, such as aTextTool align:aString at:aPoint showFrom:N, which tells a text-drawing tool to align the beginning of a string at a specified point on the screen, and show it from the nth character to the last character.

In most cases you cannot make a class or method name too long. Modern editors and IDEs virtually eliminate typing-time concerns, and names using complete words are easier to spell correctly than abbreviations. (There are obvious exceptions for widely used acronyms or abbreviations such as "DDE" or "Mac.") If you follow the previous natural syntax guidelines and the subsequent consistency guidelines, a longer name is often easier to remember than a short one.

For example, consider the method name unsetf(). This is a method on the ios class (itself not a model of clarity). ios is part of the AT&T C++ library for stream I/O. unsetf is the method used to remove previously set data-format flags and restore the prior flags. The "f" made that perfectly clear, right?

Smalltalk is not immune to poor method naming. The Collection class method inject:into: sounds understandable. Unfortunately, it doesn't inject anything into anything. What it does do is let you iterate through a Collection, processing each element in turn. The processing is done via a block of code, and the block also has access to the results of the prior iteration of processing. This makes it easy to bubble-sort a collection, for example. Apparently, the name was chosen because it rhymed with sibling methods "select," "collect," "detect," and "reject." Enough said.

Be Consistent

As users begin to learn your library, you want the knowledge to build up as quickly as possible. The key to this is consistency. Establish standards for how to handle similar situations and stick to them. For header file names (in languages that have them), for instance, establish a convention for deriving the 8.3 file name from the name of the key class or classes declared therein. For example, "irect.hpp" is a good name for the header file in which class IRectangle is declared. It is less obvious that COLETemplateServer is found not in afxole.h, but in afxdisp.h.

Some class libraries establish class-naming conventions using prefixes. Open Class prefixes all of its classes with "I", while MFC uses "C", and C++/Views uses "V". The obvious benefit here is avoiding name clashes when several libraries are used in the same application. There is an ANSI standard in the works to give separate name spaces for different libraries.

Should you capitalize class names? Each word in a class name? Should you capitalize method names? Each word (after the first) in a method name? There is no consensus on this issue. Within the Smalltalk world, classes are word-wise capitalized and methods are lowercased with subsequent words capitalized. Rogue Wave, Open Class, and C++/Views follow this same convention in C++, while MFC and Borland capitalize methods. Whatever you do, decide on it and be consistent, or you'll earn the wrath of developers constantly mistyping initial letters.

"Accessors" are methods that set the value of some data in an object or that return the current value. Often, 30 percent or more of a class's methods are accessors. Consistency here yields great benefits in interface usability. The vast majority of C-based class libraries use either the "getAbc/setAbc" scheme or the "abc/setAbc" scheme, where "Abc" is the data item. Unfortunately, there is no clear majority. In Smalltalk, the well-established convention is that method "abc" with no parameter gets the value, while "abc:" with a parameter sets the value. As long as you are consistent, and maybe even consistent with the other libraries your developer audience likes, you should be okay.

There are similar conventions for some other method categories. The two most common are "isAbc" and "asAbc". If your class has states that it can be in (such as enabled/disabled or visible/hidden), you should have methods isEnabled and isVisible, which return True or False. The choice of which Boolean state to use in the method name (isVisible or isHidden) is based on which state developers would most often expect to be true; in this way, their "if" clauses are most likely to stay simple with no confusing negative logic.

The "asAbc" convention is used for methods that return this object in a different form. For example, the Smalltalk String class has methods such as asLowerCase and asInteger. Given that a developer knows a desired type, he can make a good guess as to the proper method to call. That is, after all, the goal-behind-the-goal of library usability-design things so developers can guess the right class, method, or whatever without reading the documentation. We rarely read it anyway.

Don't Make Things "Almost" the Same

If two things appear identical, they should be identical; otherwise, you should make them clearly different. Anyone who remembers file-selection dialogs before the advent of a system standard will recognize the importance of this guideline. There is universal agreement on this principle in UI circles; there should be in class-library circles as well, particularly as applied to class and method naming.

To illustrate, I'll present a couple of bad examples. The ios class uses the operator to extract data from a stream. For example, aStreamdoc; extracts data from aStream and places it in the variable doc. On the other hand, aStreamdec; appears the same, but does a completely different thing. It turns out that dec is a special keyword signifying decimal, and this statement sets the mode of aStream to decimal. A better solution would be to have a decimalMode (spell it out!) method that sets the mode, and leave the operator to what it does best.

The choice is not always that obvious. Taligent has its own I/O stream classes, used for (among other things) persistent storage of objects. TModel is a class that is expected to stream itself out and back in on demand. It is true that when TModel streams itself out, it is not quite the same thing as a TStream object streaming via the operator. Therefore, it might not be wise to use the same operator in TModel. The choice made by Taligent was to instead use a = operator. According to our guideline, a better choice would have been streamToStore or something distinctively different.

In general, operator overrides should be carefully considered. Operators like + and > come from the world of mathematics with concrete meanings. According to the guideline on real-world knowledge, supporting these operators for nonmathematical objects (where it makes sense) is a fine idea. However, programmers who first began making up new operators (like ) opened a Pandora's box, which is only now being hammered shut by the object-oriented community. If you find yourself tempted to make up a new operator, think it through very carefully, consider the ramifications, and, if it still seems like a good idea, bang your head against a wall until the temptation passes.

"Almost the same" doesn't just apply to operators. Don't get carried away with polymorphism to the point where you are stretching the original meaning of the method name. It makes sense to "rotate" a page of text 90 degrees. It may even make sense to "rotate" a video clip. But what does it mean to "rotate" a sound clip 90 degrees? The fact that you can make up some strange waveform-manipulation algorithm is beside the point; the typical developer won't know what you mean.

Design to Prevent User Errors

Some languages are more ripe for developer errors than others, but they all could use some help. As in GUIs, the ultimate goal is to make it impossible for users to make an error in the first place.

For those languages that use includes, multiple include protection is a must. Most people have found this simple device is even simpler to manage by always naming the protect variable the same as the include file name. In C++, an include file iframe.hpp would look like:

#ifndef _IFRAME_
#define _IFRAME_
... body of include ...
#endif // _IFRAME_
In this way, you don't even have to worry about keeping track of the #defined labels.

Passing or returning a reference instead of a pointer (a distinction made in some C-derived object-oriented languages, such as C++) can avoid certain errors. By definition, a reference always points to something. If you always have something to return (that is, never a null pointer), then using a reference is a good choice to improve usability; the caller never has to worry about a jump to zero.

Regardless of whether or not you return a reference or a pointer, you are taking a usability risk in nongarbage-collecting languages like C++. You must clearly communicate the lifetime of the object being referred to and who is responsible for destroying it. Use of a pointer (or reference) to an object that has secretly been destroyed is one of the most common and difficult-to-catch errors a developer can face. A memory leak, caused when no one destroys an object, is just as bad.

So, what is the guideline? Use naming conventions, if possible, to consistently cue developers regarding ownership. For example, Taligent uses an "adopt and orphan" convention. Any method that returns object Xyz and wants the caller to take delete responsibility for that object, is named OrphanXyz; the receiver (the callee) is being asked to orphan the returned object. If delete responsibility remains with the receiver, the method is named GetXyz. Likewise, a method that takes an object as an argument, where the caller wants the receiver to take delete responsibility for the object, is named AdoptXyz(anXyz). Otherwise, it is called SetXyz(anXyz). I like this convention, although most class libraries have yet to address this situation, so there is no consensus at this time.

Default Values or Behaviors

If simpler is better, then a method with no parameters is the best method to have. One way to get more of them is to support default values for parameters. What makes a good default depends on the class and method, so there are no easy answers here. Talk to your developer-users.

Default behaviors apply to any abstract "implemented by subclass" method in your Smalltalk classes (similar in intent to a pure virtual function in C++). Because the subclass may not actually override the inherited method, your framework should, if possible, perform some reasonable default behavior. Only if there is no reasonable default should you instead throw an error. C++ is a bit nicer here, in that a pure virtual function that your developer forgets to override results in a compiler error.

This guideline also applies to a class or, more specifically, to a newly created object (instance) of a class. Any object should be as fully formed and ready to use as possible before it is returned to the caller. This makes it easier for developers to get an initial prototype of their application up and running and to gradually add code as they learn. Recent developments in some distributed-object systems have led to the use of an initialize method that must be called following creation of an object; the object is not ready to use until both the constructor and the initialization method have run. Although there may be good technical reasons for this separation, the usability problems it introduces should not be ignored.

Be Modeless

UI designers agree that designing a system with modes is usually asking for trouble. Whatever mode the system is in, users invariably want to perform some task in another mode. The solution is to keep user operations atomic and to design the system to be modeless.

This is a general characteristic of human thinking and is not confined to GUIs. We often need to do several things at once, or interact with several other processes at once. The best design in a class library or framework, then, is a modeless one. Keep methods atomic and independent. As much as possible, developers should be able to call the methods in whatever order they like. Take, for example, a File class. One design would require developers to first call open before calling read. A better design would automatically do an open if a read was called first. Even if you are a layer on top of a truly modal base (such as in the File example), it does not mean you must be modal.

Immediate and Reversible Results

In UI design, users should be able to immediately see the results of an action and should be able to undo that action. In class libraries, the equivalent also applies. When developers call a method to set a value, a subsequent get should return the new value. If there is any reason for a delay (such as time lag due to distribution across systems), the get method must account for it in some way. As distributed object-oriented systems become more commonplace, there will be more need for standards for "asynchronous" methods. These will have to balance the improved usability of synchronous behavior (or the appearance of synchronous behavior) with the benefits of distributed systems.

Reversibility also applies to class libraries. This can be achieved by having a resetAbc method along with the set and get methods. A reset method is not always needed. If the original value of this attribute is convenient to keep around (small and not shared), then the developer can reset by calling setAbc with the original value. However, there are some attributes where this is not so easy. Add a reset method in these cases. Also, there are cases where you need to set a complex group of attributes that together define the state of the object. If there is a clearly defined default state for the group, it may be awkward to keep all the individual attributes you have changed. For example, a printer usually has defaults for font, page size, layout, and many other attributes. Having a resetToDefault method would allow all the attributes to be reset at once.

Conclusion

Training yourself and your team to apply these guidelines will take time. Deciding how they apply to a particular situation is not always cut and dried, and the discussion will also take time. Only part of this time will be regained later in your development cycle. The result, however, as shown by some of the commercial tools I've mentioned, is greatly improved usability. Developer-users are recognizing that this saves them time in their development cycles. And that, after all, is the whole purpose of class libraries and frameworks.

Acknowledgments

My thanks to the development teams of all the class libraries whose guidelines and conventions (whether intentional or accidental) I used as examples. Special thanks to Bob Love, who champions many of the guidelines we use on the IBM Open Class development team.