API Usability: Guidelines to improve your code ease of use

Transposing the usability principles, traditionally used in GUI design, to library and component interface design

Introduction

High usability is becoming increasingly important for a product to succeed. People want technology that is easy to learn and use: just a lot of functionalities are no more sufficient. Because the usability concepts are based on the human psychology, they can be applied to the design of all interfaces between humans and machines. In the recent years, the software engineering field has successfully adopted these concepts. One man who has highly contributed to the promotion of software usability is Jef Raskin. He has acknowledged the importance of usable software since a long time and has applied the usability concepts to design the first commercially successful computer to use a graphical user interface (GUI). Since then, usability research results have provided many hints to help software developers designing better GUI.

But the usability concepts have not to be limited to GUI design. The current trend on component development, addressing the lack of reuse in the software industry, has changed the day-to-day tasks of software developers. Now, developers must integrate third-party components in their applications and consequently, have to deal with several component interfaces (also named application programming interface or simply API). Clearly, an API can be considered as a human-machine interface and thus usability principles can be applied to ease the task of software developers. But curiously very few API design guidelines have been developed yet.

This article aims at deriving an API design checklist from the general usability principles. At Codeproject particularly, that checklist can be used to greatly improve the quality of the libraries and components available to the community. The article is divided in three sections. The first section presents a definition of usability and its fundamental principles. The second section applies the usability principles to the context of API design and gives concrete guidelines. Finally, the last section synthesizes the guidelines in a more checklist-oriented way, allowing it to be used in inspections, walkthroughs and reviews.

Usability focuses on the users and the ease of interacting with a system. ISO 9241 defines usability as "The effectiveness, efficiency and satisfaction with which specified users achieve specified goals in particular environments". Effectiveness is the accuracy and completeness with which the user tasks can be completed. Efficiency relates to the ratio of the effectiveness to the used resources. For example, the ratio of the number of execution errors to the time it takes to perform a task. User satisfaction is the subjective perceived usability level of a system by the users.

Jacob Nielsen, a recognized usability expert, defines usability as how easy, efficient and pleasant it is to use a service. These two definitions are very similar and very user-centric. In fact, the most important thing to remember is that usability is about the user and not the machine internal construction! That means that sometimes the machine development has to be more complex in order to give to the user a better experience.

How usable is a system? The preceding usability definitions contain different qualities that need to be measured in order to be able to give a usability rating. Usability expert B. Shneiderman uses the following measures:

Time to learn a system: The time or resources needed to remember how to perform some tasks.

Speed of task execution: The time or resources needed to perform a specific task.

Rate of errors: The ratio of the number of mistakes made during the execution of a task to the total number of actions needed to perform the task.

Knowledge retention over time: The time taken to forget how to perform a task.

Subjective user satisfaction.

According to these measures, to have a maximum usability score a system design must minimize the learning time, the rate of errors and the execution resources, while maximizing the knowledge retention and the user satisfaction. The perfect score is seldom achievable since design involves a lot of tradeoffs. For example, to minimize the rate of errors the design could carefully guide the user throughout the task execution (i.e. use of wizards), thus diminishing the execution speed.

The usability of a system for a set of tasks can be approximated by making the hypothesis that less a task execution is error prone and more it is simple and easily learned, then less will be the execution time and the resource needed. Knowing that frequent tasks are more annoying for the users if they are less usable, then the following formula can be used:

This formula sums the products of all task frequencies and execution times. The nearest to zero, the better the usability result is. It is interesting to note that to minimize this formula, we can minimize three things: the number of tasks, the task frequencies and the task execution times. These observations are useful to derive design guidelines.

Following the preceding usability measures and using the knowledge of the human psychology, it is possible to pose general usability principles, which could be applied to the design of all human-machine interfaces. This is exactly the exercise that the usability guru D. Norman has done. According to him, there are four principles that lead to highly usable systems:

Good visibility: The possible actions, the states and the alternatives for action must be exposed.

Good conceptual model: The use of helpful, consistent and complete abstractions helping the users to create a proper mental image model of the system.

Good mapping: Natural mapping between actions and their results.

Good feedback: Full and continuous feedback about the results of actions.

When all actions are visible there is no need to remember what the system can do. A counter example is a telephone with only two buttons to represent numbers from 0 to 9 and other special commands: "button A" and "button B". In this case users would have to learn and remember the pushing sequences for all symbols (maybe: B for 0, A-B for 1, A-A-B for 2…). The result is that it is hard to tell if this phone has a specific feature like redial or hold because the only things that are visible is an "A" and a "B".

Good visibility reduces the learning time and augments the retention time. It can also reduce the time it takes to perform a task by making frequently used controls very accessible. For example, if you often brush your hair in the morning, then put your brush right in front of your bed to make sure you don't have to spend too much time looking for it.

It can also be said that it reduces the error rate by preventing misinterpretation of the system state. This is the classic example of modes: the same action yields different results depending on the system mode. For example, in a word processor software when you type a text and you inadvertently push the "insert" key, then the next keystrokes will overwrite the text already written. This is very bad because you only know that the system is in "overwrite" mode when you've already made a mistake. In this situation we can say that the system state is not properly visible.

But since design is a tradeoff exercise, it is good to know that visibility taken to an extreme has some drawbacks. Imagine that you have one hundred or more features in your product. Would you put one control per feature in your product interface just to make sure all are visible? With as many visible items, the user may have some difficulty finding the right control, because the time it takes to find a control is proportional to the number of controls. One way to reduce the number of controls is to organize them in several categories, thus allowing the user to concentrate on only a subset of the controls at a time. Of course the categories must be meaningful.

A conceptual model is the user's system image. With this image, the user can predict its organization, its responsibilities and the results of performing certain actions. If the conceptual model is consistent and complete, then the user will feel comfortable and will not fear unknown situations. A counter example is a complete black box with only one button that does random things each time it is pressed. There is no way for the user to be sure of the results and if the box stops to work for some obscure reasons, then the user can do nothing but find another box.

One key thing is to design around concepts that the user is familiar with. This way, the learning time is reduced and the retention time is increased. For example, if you develop a replacement system, it is a good idea to mimic the existing system. But it is also desirable to adopt or invent new metaphors in order to reduce the task execution times or optimize any other usability characteristics, trading off some learning time.

Consistency is very important in order to let the user learn one general concept and apply it to specific situations. Without consistency, the user can't generalize and has to think on a case-by-case basis, applying blindly what the system instructions say. Such a system puts a big burden on the user memory and makes exploration very difficult because the results of actions are not predictable, nor logical.

Mapping relates to the relation between two things. If this relation is arbitrary then the mapping is not good. For example, why on a particularly unusable phone, composing # followed by 5 and 0 puts a phone call on hold?

Natural mapping is highly desirable since it uses physical analogies to link actions and results. For example, if a control is an arrow pointing upward, then pushing it should mean moving something up. Natural mapping, as the preceding principles, reduces the learning time and increase the retention time. But, unlike the other principles, the retention time can be infinite because nature is very stable over time.

The execution time can also be reduced because the user doesn't have to think about the results of its actions. The better the mapping, the less the user has to think. The same argument is used to say that the error rate is also reduced. For example take a shopping center entrance door. How many times have you tried to push it when you should have pulled it instead? This is a very frustrating example of a lack of natural mapping. The fact the most doors have a text instructing the user to "push" or "pull" indicates that usually doors are badly designed. The main reason is that the shape of the door doesn't give a hint to the user about its proper use. A natural mapping for "push" would be to put a plate at the position that the user has to push. In the same manner, the "pull" action could be easily mapped to a vertical grab bar. If the mapping were natural, then there would be no more embarrassing error when trying to open the door to your loved one!

Feedback allows the user to know if the desired result has been obtained. Knowing this, the user can use the system more adequately (i.e. try alternative actions) and be confident that the system is working properly. In many situations, good feedback means good responsiveness. For example, a computer system should not freeze while it is processing a user command. At a minimum the system should indicate the state of the processing like "x% done".

In general, all system changes that impact the user should be visible at the right place and moment. The feedback speed must be adapted to the task and to the user. For example visual feedback speed has to respect human visual recognition capacity that says that nobody will see an image visible only during 1/600s.

Good feedback also helps users exploring a system, consequently reducing the learning time. Without feedback a user can't know if he's doing right or wrong, or he may know too late in order to be able to correct his action. As a result, he will fear experiencing the system himself and will need a kind of teacher.

Another benefit is the reduced error rate. Because the user knows at the right moment that something happened, he can do the appropriate actions and avoid errors.

An application programming interface (API), as its name implies, can be considered to be a user interface in the same way that a GUI is the interface between some software and its operators. In the case of an API, the user is a programmer that uses the API to accomplish his programming tasks. Instead of pushing buttons and selecting items in combo boxes like a GUI user, the API user writes code. A usable API reduces the time it takes to learn how to program against it (learning time), it reduces the number of lines of code to write (execution time), it reduces the number of wrong interpretations and misuses (error rate) and allows the easy understanding of code although written a long time ago (knowledge retention). Consequently, the programmer is happy (user satisfaction!). Following the four general usability principles, several API usability guidelines can be stated. These guidelines have been developed with object-oriented concepts and languages in mind, thus narrowing their scope but increasing their usefulness in this specific context.

Using the object-oriented terminology, an API consists of classes, methods and data structures. Usually, the user can see the API constitution by browsing its declaration files. In this context, API usability design guidelines target the organization of these files, which are the only observable elements in the user point of view (i.e. the façade). That is not to say that the internal library or component design is out of scope because, in a reduced scale, all class declarations can be considered as a little API to be used by the developers of the library, if not accessible to the user.

The next sections present the API design guidelines associated with each Norman's usability principle.

The information that is the most important for the user should be the most visible and accessible. Generally speaking, the most important things are the proper execution of the user tasks. Consequently, classes, methods and data structures related to the most frequent and important tasks should be very visible.

Put public declarations first, then protected and private

In the case of a class declaration, the user is more often interested in which services the class offers than to know private or internal data. Most people read from top to bottom, thus the topmost information should be the most important. Also, declaration scopes should not be mixed. It is better to declare all public things at the same place, henceforth for other scopes.

Put the declaration of the methods corresponding to the most frequently used tasks first

There is more chance that a user needs to consult the documentation of common tasks than very rare ones. This is in line with the usability formula, since it reduces the time taken to execute frequent tasks. But it comes at a cost: it takes more time to find the documentation of less frequently used methods. A little example of bad placement is to put the class destructor at the top section of the class declaration. Most of the time, the user doesn't care about the class destructor.

Put the documentation in the API declaration

Some developers have the habit of putting comments only in the implementation file (for example cpp files in c++). This may be fine when the implementation files are always accessible, but anyway, this is easier for a user to look at the API declaration files than to open the implementation files and having to browse through all the code.

Separate declaration and implementation

Most of the time the user doesn't care about how things work, he only wants to know the available methods and their signatures. The less code he has to read to find a method declaration, the better. Consequently, implementation details are not desired in declaration files. A subtle case of unwanted implementation details is when a comment contains maintenance-specific information, for example, design rationale or algorithm pseudo-code. This information should be moved to the implementation files.

Avoid non task-oriented information before API declaration

When opening a class file, most of the time the user wants information about performing some tasks. The first thing that the user sees is the upper part of the file, thus the more important information should be there. Two examples of not very useful information are history and legal notice. It may be better to put the history and the legal notice at the end of the file instead.

Less is better since, according to the Hick-Hyman law, a too big API will confuse the user.

Group public methods around tasks

Organize the public methods of a class by making groups around user tasks and make it clear which tasks a group represents (i.e. use comments as you would do with a method). This allows the user to concentrate on his task, not having to look at the whole API at once. The following code shows tasks highlighted with special comments and alignment:

Remove unused features

A mode is introduced when a control do different things depending on the system state. In the context of an API, a control is a class or a method. When some state information is needed in order to understand what an object or a method call purpose is, that means there is a mode.

Avoid using methods that do different things based on the object state

If a method does different things based on the object state, this is clearly a mode. In this case, the user has to know the current state in order to be able to predict the method result. If the user misinterprets the state, errors will happen.

Avoid using optional method parameters

If optional parameters are omitted, the method behavior depends on a default value that is defined somewhere in the object. This default behavior is not visible when reading the code and can also depend on the object state. Thus, the user must remember what is the default "mode" and change it if not appropriate. Instead of using optional parameters, it may be better to make one method per behavior or mode.

Conceptual model is highly related to the object oriented principle of deriving classes from the user world. There must be a global metaphor guiding the organization of the API. That metaphor helps the user to navigate through the API and understand each part and their interrelations. Consistency is of utmost importance since it assures that syntactically similar things have the same semantic.

Name classes and methods around tasks

Since the only thing that the user care about is executing his tasks, it is logical to name things around those tasks. If there are some difficulties in naming things around tasks, then this may be that the conceptual model is not adequate (for example, it can be too abstract or not enough).

Consistency, at best, should also be logical. When there is no logic, then a kind of standard can help. But randomness must be avoided at all cost.

Syntactically similar entities should have the same semantic

In other words, if two entities look the same, they should have similar meanings. Entities refer to class names, method names, name prefixes and so on. This is a basic consistency requirement.

Follow a coding standard

A coding standard ensures a consistent syntax. Particularly, it imposes a naming convention and an organizing scheme.

Keep the same ordering

A consistent ordering helps the user to predict what comes next and where to find what he's looking for. Often the coding standard specifies the proper ordering. Examples of ordering are:

Class declaration order: a class declaration is composed of many elements that should always appear in the same order. Examples of elements are public, protected and private items, constructor and destructor, specialized and overridden methods.

Method call order: for example if many classes have an "init" method and if one class needs that the method be called first, then it should be the same for all classes.

Comment content ordering: method comments are composed of many elements like a general description, parameter descriptions, exception descriptions and so on.

Method parameter order: for example, if some methods need an object id, then this id should always be at the same position in the parameter list.

Too much abstraction puts excessive burden on the user because he has to do extra work to specialize the API. Inversely, if there is not enough abstraction the user will not be able to execute his tasks in his specific context.

The code needed for the most frequent tasks should be minimal

Frequent tasks should be directly executable from the API, without needing a lot of extra code. However, less frequent tasks, like API extensions, may necessitate special code to be adapted to the specific user environment.

User code duplication should be minimal

If the user needs to duplicate a lot of code this means the abstraction level doesn't support well the user tasks.

The user has to do many associations in order to be able to use the API properly. There are links between classes and their responsibilities, between methods and their results and also between data structures and their usage context. Good mapping is about making these links very explicit.

High class cohesion

Class cohesion relates to the level of which the class methods are linked together semantically. If there is a low cohesion, then the user will have difficulties linking the class and what it does. This is also related to a design rule that says that a class should have only a few clear responsibilities. Too many responsibilities lead inevitably to low cohesion since each responsibility has its own goals and necessitate a different set of methods.

Avoid using magic values

Magic values are those special symbols that say nothing about their meaning. For example, consider the method "void remove(bool last)" that removes the first or the last element of a list. It's very difficult to say, without looking at the method declaration, what "remove(true)" means. There is no direct link between "true" and the removing of the last element.

Use the most appropriate data types for the situation

When natural data types are used, the user doesn't need to consult any documentation to know what is the proper type to use because he can deduce it logically. For example, if a value cannot be negative, then the data type should not allow negative values ("int" type vs "unsigned int" type). Another bad example is the use of the "void*" as a method parameter type like: int doSomething(void *param). In this case, any object types can be passed to this method, which doesn't help the user to figure out what this method needs. The result is extra documentation to write.

Write self-explaining code

Self-explaining code means that just looking at something is enough to know what it does and that no supplementary documentation is needed. This doesn't mean that comments are prohibited but that efforts should be done to improve the code clarity thus reducing the amount of external documentation needed. This is also a visibility issue since if comments are needed that means the code doesn't expose its entire capabilities or restrictions.

When a programming error occurs, the fastest an error report is provided, the better. This is because it is easier for the user to associate an error with the action he's doing right now.

Validate user input immediately

To be useful the error must be reported as close as possible to the faulty code. For example, if the user has to call a method to provide data for the execution of another method, then the data validation should occur at the moment it is provided instead of at the execution time.

Bad example: trying to open a invalid file

File.open("filename");
File.read(…); // Exception occurs here!

Better example:

File.open("filename"); // Exception occurs here

Use code assertions

Assertions validate that the API internal state is consistent and that the user action can be properly executed in the current context. It is better to not allow inconsistent states to happen in the first place (use forcing functions or hide unavailable methods), but when it is too complicated, instead of continuing the execution in an invalid state, it is better to immediately stop the execution and tell the user what is wrong. Assertions can be used to check invariants, pre-conditions and post-conditions in method calls.

Debugging information helps the user to explore the API. It allows him to try new things and to receive detailed programming feedback.

Tell why and where an error occurred

When the user is trying something, he knows what code he is typing but may not understand why it doesn't work. This is in these situations that a "why" is very welcomed. The "where" is also important because it clearly points to the faulty code part. For example one method call may contains many parameters and it may be difficult to say which parameter has caused the error.

Bad example:

File.Open("filename", WRITEACCESS); -> "Cannot open the file."

What is wrong? Is it the specified filename or the requested write access?

Better example:

File.Open("filename", WRITEACCESS); -> "Cannot open the file 'filename'
because it is used by another process."

Obtaining feedback is a very frequent task and at best, it should be continually transmitted. Consequently, the code needed to obtain the API state should be minimal.

Provide a default textual description with all errors

When an error occurs it is more convenient for the user to have a textual description of the error and its cause than just an error number. With the later, the user has to call an auxiliary method to translate that number into a readable message, which is not very convenient when just exploring the API. When using the exception mechanism to provide error information, the default error message can simply be a supplementary field in the exception structure.

The user error handling code should be minimal

This code is needed practically each time an API call is made, thus it is better having to duplicate a minimum amount of code. Exception-oriented APIs often necessitate a lot of error handling code, particularly when there are many exception types. There is a duplication problem when the user doesn't care about all the special cases and only wants to know whether the thing he asked for is correct. In this case, the use of an exception hierarchy can greatly reduce the burden on the user by allowing the handling of only one general exception.

Although the presented checklist is certainly not complete, it contains basic checkpoints that if applied can greatly improve the usability of a programming interface. An API doesn't have to be huge for the usability principles to be useful. For example, often developers make little pieces of code (a class or two) that are used by their colleagues and I think not many people consider usability when designing their class interfaces, but they should.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

About the Author

Comments and Discussions

I think I'll print it out and use it (one way or another) in our new-developers courses.
Nevertheless I see one important point that is not mentioned in the article, namely: Prefer compile-time errors over run-time errors.
This rule becomes clear in one of the examples you give. In the factory example you prefer the following construction:Object.setFactory(NULL); // assert(factory!=NULL)
Over the following construction:Object.setFactory(NULL); // Oups! Cannot be null…
…
MyObject *pObj = Object.newObject(); // Crash!
Although the first one is better than the second one, in both situations the problem is only found at run-time, which is too late. It is better to replace this construction with one that finds these errors at compile-time, like shown in this class-definition:class MyClass
{
public:
void setFactory(const MyFactory &myFactory);
};
In this case, setFactory can never be called with a NULL-pointer. In fact, this error is found even before the code is compiled, since the developer should realize that he can't give a NULL-pointer to it (so it's found at development-time, rather than run-time).

You are right to say that compile-time error detection offers many advantages over run-time error detections. I think this is another example of early feedback. And moreover, this is an automated verification tool.

Are you a teacher? I would be happy to know what your students think about these guidelines.

No, I am not a teacher, but a development manager in a small software company.
We always try to keep an evolving set of style guide rules for our developers. Not that we extend our style guide daily, but I try to keep an eye on interesting rules found on the Internet.