Striving for consistency

Being consistent means doing the same thing the same way every time. The human brain is wired to look for patterns and rules because our ability to predict future events (the ripening of fruits, the start of the rainy season, or the migration of animals) has been essential to our survival. Our minds work the same way when developing software. When you see the interface names AssetServices, MetadataServices, and ContentServices, what do you expect the video interface to be called? Isn’t it true that you feel reassured and encouraged when you find the VideoServices interface? Inconsistency doesn’t mean complete chaos and confusion. In an inconsistent world, rules, patterns and conventions are still discernible, but there are numerous unpredictable and inexplicable exceptions.

We call an API consistent when there are no frivolous or unnecessary variations in it. We quickly become familiar with such APIs because they are predictable, easy to learn and remember. Their consistent behavior gives us confidence that we can use them correctly.

Following conventions

Many well-known coding conventions were adopted with the sole purpose of minimizing small, but annoying variations in programs. Pascal casing is no better than camel casing; yet we call our method RemoveTag() in .Net and removeTag() in Java, because otherwise we violate  established conventions and introduce inconsistencies. We name our interface IPublishable in .Net and Publishable in Java, regardless of what we think of the use of “I” to distinguish interface names from class names. We use Hungarian notation when interacting with low-level Windows API functions from C code, even though we consider Hungarian notation a hopelessly outdated annoyance. This is not only true for large platforms, but for smaller APIs as well. We follow established conventions, sometimes silly ones, whether we agree with them or not.

Some APIs are inconsistent by design, but it is far more common for inconsistencies to creep in with subsequent modifications. Consider the following example:

   public interface Capabilities {
      public boolean canCreate();
      public boolean canUpdate();
      public boolean canDelete();
      public boolean canSearch();
      public boolean canSort();
      …
      public boolean isRankingSupported();
   }

The last method looks dreadfully out of place. It is pointless to argue which of the two naming conventions is better. Reverse them and the interface still looks bad. Novice developers are especially prone of engaging in such never-ending, fruitless arguments, not realizing that consistency often trumps other considerations. When adding a new method to an existing interface, simply follow the conventions already in place.

Adopting conventions

De-facto conventions are already in place for many existing APIs. For new APIs, especially large APIs, we need to adopt and document our own conventions. It is almost entirely up to us what conventions we use, provided that they:

  • do not contradict the established conventions of the chosen development platform
  • aim to minimize unnecessary variations
  • do not impose any real restrictions on functionality

For example, a potential for unnecessary variations exists in parameter ordering. We can see this in the C standard I/O library functions, where fgets and fputs have the file descriptor as the last parameter and fscanf and fprintf have it as the first, frustrating millions of developers for more than 30 years. Establishing a convention for parameter ordering eliminates such variations without restricting functionality.

A lot of gratuitous variations can creep into an API concerning the usage of null. Every time a method takes an object parameter, we should know if it accepts null or not. If it doesn’t accept null, we often see unnecessary variations in how the error is handled. If null is accepted, we again see many variations in what this actually means. For methods which return an object reference, we need to know if it ever returns null, and if it does, when and what does it mean? Conventions regarding the usage of null can be helpful in avoiding such uncertainties.

We should keep in mind that we are establishing conventions and not strict rules. We may be tempted to enforce rules like “No method should ever return null; it should either return a valid object or throw an exception” because it is not only consistent behavior, it also makes the API safer to use. The problem is that there are justified deviations from this convention. What should a method designed to look up a specific object do when it doesn’t find it? As a rule (yes, this is a rule), we should only throw exceptions under exceptional circumstances. Looking for something and not finding it can be anticipated and it shouldn’t cause an exception. While there are certain other design options, none of them are as simple as returning null. Consistency is about removing unnecessary variations and there are cases where variations are warranted. “Extreme advice is considered harmful” warns Yaroslaw Tulach in his book Practical API Design.

Using patterns

Patterns can remove further variations from APIs. Unlike the “Gang of Four” design patterns, which are recipes for solving specific design problems, API patterns are used to make large APIs more predictable. In this context, the standard dictionary definition of the term, “elements repeating in a predictable manner”, is used.  API patterns are formed using repetition, periodicity, symmetry, mirroring, and selective substitution as seen in patterns of nature or in decorative arts. We can borrow API patterns from others or make up our own. Since we need predictable APIs, not decorative ones, the simplest patterns are the best.

For example, one of our APIs consists of only two kinds of objects: service objects and data objects. The service objects are named by appending “Services” to the service name (AssetServices, MetadataServices, and so on) and are placed in Java packages that end with “.services”. Every service object is a singleton and can be instantiated calling the static getInstance() method. The data transfer objects have the words “Request” or “Result” appended to their name, like in ExportRequest and ExportResult. When the request has search semantics, the data object is named by appending “Criteria” to the name, for example, RetrieveAssetCriteria. Such patterns are great in large APIs, where simple coding conventions leave plenty of room for other, higher-level discrepancies.

In addition to structural patterns as above, we can establish behavioral patterns. In our API some methods are optional and, depending on the server configuration, they may work or throw an UnsupportedOperationException. There is a Capabilities interface (shown above), with methods like canSearch(), canSort(), or canUpdate(),  which can be called to check if some functionality is available or not. Consistent use of structural and behavioral patterns can make even very large APIs easy to use, since what we learn from using one part of the API can be easily transferred to other parts.

Enforcing consistency

Patterns and conventions have to be enforced when working in large teams because inconsistencies are very likely with several people contributing to the design. API design as a whole should remain a team effort, but ideally a single individual should be responsible for its consistency. This person should be authorized to review, accept, or reject API changes, but – and this is very important – only for consistency reasons. This role is a consistency advocate, not a supreme design guru. For example, Brad Abrams and Krzysztof Cwalina became well-known inside and outside Microsoft after they were appointed to ensure the consistency of the .Net platform. Joshua Bloch had a similar – albeit unofficial – role in the core Java API development while at Sun. Having a reviewer to find and correct inconsistencies and an independent arbitrator to stop the team from wasting time on unproductive disputes can be very helpful.

Compromising

Consistency is so important that it is worth compromising in other areas to achieve it. To put it simply, using the same design everywhere is often better than choosing the best solution for each particular case. For example, exceptions are preferable to error codes, but it is a lot easier to work with error codes than with a mix of error codes and exceptions. We like collections more than arrays, but we like it even less when they are mixed together. This can happen when we try to “improve” the design as the API evolves. We can’t change the old parts due to backwards compatibility requirements, and if we use a different, “better” design for the new parts, we introduce inconsistencies. Right now, one of our APIs is caught right in the middle of such ill-advised migration from arrays to collections.

Avoiding misleading consistency

We should be careful not to introduce false or misleading consistency. Misleading consistency is like false advertising or a broken promise. For example, if there is an interface named Driver and a class named AbstractDriver in the API, developers will expect that AbstractDriver implements Driver and they can inherit from it to create their own implementations. If this is not the case, it is better to name either the class or the interface something else.

Also, we should reserve the standard JavaBeans getter and setter method names for methods accessing local fields. There is nothing more frustrating than to call a seemingly harmless getAssociations() method, watch it block for 25 seconds then see it throw a RemoteException. A different name, like retrieveAssociations() would signal the real behavior much better.

We create false expectations of consistency when our design is consistent only in certain aspects and inconsistent in others. For example, we follow consistent naming conventions, but have no consistent type structure, parameter ordering, error handling or behavior. New team members are the most likely to commit this mistake, because naming conventions and structural patterns are significantly easier to follow than consistent behavior.

Conclusion

The benefits of consistent APIs are obvious and consistent APIs don’t take more time or effort to design than inconsistent ones. We only need to adopt and follow certain patterns and conventions. APIs can be reviewed and inconsistencies corrected even late in the design process. The only essential requirement for consistent API design is discipline. This makes the “strive for consistency” API design guideline the easiest to follow.

Creative Commons Licence
This work is licensed under a Creative Commons Attribution-ShareAlike 2.5 Canada License.

Advertisement

About Ferenc Mihaly
Senior Software Architect at OpenText Corporation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: