The Artima Developer Community
Sponsored Link

Dr. Dichotomy's Development Diary
Java API Design Guidelines
by Eamonn McManus
December 28, 2005
Summary
There are tons of books and articles about how to design and write good Java code, but surprisingly little about the specific topic of API design. Here's a summary of what I've learnt on the subject from various sources and my own experience.

Advertisement

I recently attended an excellent talk at JavaPolis, Elliotte Rusty Harold's XOM Design Principles. Although the talk is nominally about XOM (an API for XML documentation manipulation), in fact more than half of it is about API design principles in general. This is a curiously neglected subject. There are tons of books and articles about how to design and write good Java code, but surprisingly little about the specific topic of API design. Yet with the proliferation of new Java APIs, whether through JSRs or through Open Source projects, this is an increasingly important subject.

I've been closely involved with the evolution of the JMX API for over five years and have learnt a great deal about what works and what doesn't during that time. During the talk, I had the odd experience of continually wanting to cheer as Elliotte made point after point that I hugely agreed with.

I'm going to try to summarize here what I see as being the key points from this talk, from my own experience, and from a couple of other sources:

[Update: Although I was unaware of it when writing this blog entry, the slides referenced by Josh Bloch in a comment here cover some of the same ground and add much of interest.]

Design to evolve

If your API is worth anything, it will evolve over time. You should plan for this from the outset. A key part of the planning is to decide what sort of compatibility you will guarantee between revisions.

The best approach is to say that once something is in the API it will stay there and it will continue to work. Tweaking the API incompatibly between revisions will result in user reactions ranging from annoyance to murderous rage. The problem is particularly severe if your API ends up being used by different modules that are part of the same application. If Module 1 uses Commons Banana 1.0 and Module 2 uses Commons Banana 2.0 then life will be a whole lot easier if 2.0 is completely compatible with 1.0. Otherwise your users risk wasting huge amounts of time tweaking classpaths in a futile endeavour to make things work. They might end up having to play mind-destroying games with class-loaders, which is a clear signal that you have failed.

For APIs that are part of Java SE, we have an extreme form of compatibility. The aim is that no code whatsoever should break when you update from one version to the next. This means that classes and methods are never removed. It also means that we try to avoid changes that might break code that was depending on certain implementation details, even if the code shouldn't have been doing that.

The no-code-breakage rule applies to already-compiled code (binary compatibility). In some rare circumstances we might make changes that mean some existing code no longer compiles (source compatibility). For example, adding an overloaded method or constructor can sometimes produce ambiguity errors from the compiler when a parameter is null. We do try to find a way to avoid changes that break source compatibility in this way, but sometimes the best approach does imply that some source code might stop compiling. As an example, in Java SE 6 the constructors for javax.management.StandardMBean have been generified. Some existing source code might conceivably stop compiling because it does not respect the constraints that are expressed using generics here, but that code is easily fixed by adding a cast, and the rare cases where that happens are outweighed by cases where the constraints will catch programming errors at compile time.

In general, you can't know what users of your API will do with it. When contemplating a change that might break existing code, you have to reason conservatively. Only if you can honestly say that it is next to impossible that a change will break code can you reasonably make it. You should certainly rule out completely a signature change, which basically means removing or renaming a visible method or class or changing the parameters of a visible method. (But you can remove a method if it overrides a method in a parent class without changing the parent method's semantics.)

Since the very earliest versions of your API are sure to have many mistakes in them, and you don't want to freeze those mistakes for all time, it's a good idea to bring out one or more 0.x versions before the 1.0 version. Users of these versions know that the API is unstable and won't curse you if it changes. Once you've brought out 1.0 you're committing to compatibility. For APIs that are developed through the JCP, these 0.x versions correspond to the phases before the final release (Early Draft Review, Public Review, Proposed Final Draft). If possible, it's a good idea to make an implementation of the API available at the same time as these intermediate specifications.

If at some stage you decide that there's really too much accumulated cruft from previous versions and you want to start over, then create a new API with different package names. Then code that uses the old version and code that uses the new version can co-exist easily.

API design goals

What should the design goals of your API be? Apart from compatibility, the following goals from Elliotte's presentation seem like an excellent set:

Be minimalist

Because of the compatibility requirement, it's much easier to put things in than to take them out. So don't add anything to the API that you're not sure you need.

There's an approach to API design which you see depressingly often. Think of everything a user could possibly want to do with the API and add a method for it. Toss in protected methods so users can subclass to tweak every aspect of your implementation. Why is this bad?

The right approach is to base the API on example code. Think of problems a user might want to solve with the API. Add just enough classes and methods to solve those problems. Code the solutions. Remove anything from the API that your examples don't need. This allows you to check that the API is useful. As a happy side-effect, it gives you some basic tests. And you can (and should) share the examples with your users.

Interfaces are overvalued

There's a certain style of API design that's very popular in the Java world, where everything is expressed in terms of Java interfaces (as opposed to classes). Interfaces have their place, but it is basically never a good idea for an entire API to be expressed in terms of them. A type should only be an interface if you have a good reason for it to be. Here's why:

Of course, there are sometimes good reasons for a type to be interface. Here are some common ones:

Be careful with packages

The Java language has fairly limited ways of controlling the visibility of classes and methods. In particular, if a class or method is visible outside its package, then it is visible to all code in all packages. This means that if you define your API in several packages, you have to be careful to avoid being forced to make things public just so that code in other packages in the API can access them.

The simplest solution to avoid this is to put your whole API in one package. For an API with fewer than about 30 public classes this is usually the best approach.

If your API is too big for a single package to be appropriate, then you should plan to have private implementation packages. That is, some packages in your implementation are excluded from the Javadoc output and are not part of the public API, even though their contents are accessible. If you look at the JDK, for example, there are many sun.* and com.sun.* packages of this sort. Users who rely on the Javadoc output will not know of their existence. Users who browse the source code can see them, and can access the public classes and methods, but they are discouraged from doing so and warned that there is no guarantee that these classes will remain unchanged across revisions.

A good convention for private packages is to put internal in the name. So the Banana API might have public packages com.example.banana and com.example.banana.peel plus private packages com.example.banana.internal and com.example.banana.internal.peel.

Don't forget that the private packages are accessible. There may be security implications if arbitrary code can access these internals. Various techniques exist to address these. The NetBeans API tutorial describes one. In the JMX API, we use another. There is a class javax.management.JMX which contains only static methods and has no public constructor. This means that user code can never have an instance of this class. So in the private com.sun.jmx packages, we sometimes add a parameter of type JMX to sensitive public methods. If a caller can supply a non-null instance of this class, it must be coming from the javax.management package.

Other random tips

Here are some other random tips based on our experience with the JMX API and on the sources I mentioned.

Immutable classes are good. If a class can be immutable, then it should be. Rather than spelling out the reasons, I'll refer you to Item 13 in Effective Java. You wouldn't think of designing an API without having this book, right?

The only visible fields should be static and final. Again this one is pretty banal and I mention it only because certain early APIs in the core platform violated it. Not an example to follow.

Avoid eccentricity. There are many well-established conventions for Java code, with regard to identifier case, getters and setters, standard exception classes, and so on. Even if you think these conventions could have been better, don't replace them in your API. By doing so you force users to throw away what they already know and learn a new way of doing an old thing.

For instance, don't follow the bad example of java.nio and java.lang.ProcessBuilder where the time-honoured T getThing() and void setThing(T) methods are replaced by T thing() and ThisClass thing(T). Some people think this is neato-keen and others that it is an abomination, but either way it's not a well-known idiom so don't force your users to learn it.

Don't implement Cloneable. It is usually less useful than you might think to create a copy of an object. If you do need this functionality, rather than having a clone() method it's generally a better idea to define a "copy constructor" or static factory method. So for example class Banana might have a constructor or factory method like this:

      public Banana(Banana b) {      // copy constructor
    	  this(b.colour, b.length);
      }
      // ...or...
      public static Banana newInstance(Banana b) {
    	  return new Banana(b.colour, b.length);
      }
    

The advantage of the constructor is that it can be called from a subclass's constructor. The advantage of the static method is that it can return an instance of a subclass or an already-existent instance.

Item 10 of Effective Java covers clone() in excruciating detail.

Exceptions should usually be unchecked. Item 41 of Effective Java gives an excellent summary here. Use a checked exception "if the exceptional condition cannot be prevented by proper use of the API and the programmer using the API can take some useful action once confronted with the exception." In practice this usually means that a checked exception reflects a problem in interaction with the outside world, such as the network, filesystem, or windowing system. If the exception signals that parameters are incorrect or than an object is in the wrong state for the operation you're trying to do, then an unchecked exception (subclass of RuntimeException) is appropriate.

Design for inheritance or don't allow it. Item 15 of Effective Java tells you all you might want to know about this. The summary is that every method should be final by default (perhaps by virtue of being in a final class). Only if you can clearly document what happens if you override the method should it be possible to do so. And you should only do that if you have coded useful examples that do override the method.

Summary

Talk Back!

Have an opinion? Readers have already posted 34 comments about this weblog entry. Why not add yours?

RSS Feed

If you'd like to be notified whenever Eamonn McManus adds a new entry to his weblog, subscribe to his RSS feed.

About the Blogger

Eamonn McManus is the technical lead of the JMX team at Sun Microsystems. As such he heads the technical work on JSR 3 (JMX API) and JSR 160 (JMX Remote API). In a previous life, he worked at the Open Software Foundation's Research Institute on the Mach microkernel and countless other things, including a TCP/IP stack written in Java. In an even previouser life, he worked on modem firmware in Z80 assembler. He is Irish, but lives and works in France and in French. His first name is pronounced Aymun (more or less) and is correctly written with an acute accent on the first letter, which however he long ago despaired of getting intact through computer systems.

This weblog entry is Copyright © 2005 Eamonn McManus. All rights reserved.

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use