Defensive API evolution with Java interfaces
API evolution is something absolutely non-trivial. Something that only few have to deal with. Most of us work on internal, proprietary APIs every day. Modern IDEs ship with awesome tooling to factor out, rename, pull up, push down, indirect, delegate, infer, generalise our code artefacts. These tools make refactoring our internal APIs a piece of cake. But some of us work on public APIs, where the rules change drastically. Public APIs, if done properly, are versioned. Every change – compatible or incompatible – should be published in a new API version. Most people will agree that API evolution should be done in major and minor releases, similar to what is specified in semantic versioning. In short: Incompatible API changes are published in major releases (1.0, 2.0, 3.0), whereas compatible API changes / enhancements are published in minor releases (1.0, 1.1, 1.2).
If you’re planning ahead, you’re going to foresee most of your incompatible changes a long time before actually publishing the next major release. A good tool in Java to announce such a change early is deprecation.
Interface API evolution
Now, deprecation is a good tool to indicate that you’re about to remove a type or member from your API. What if you’re going to add a method, or a type to an interface’s type hierarchy? This means that all client code implementing your interface will break – at least as long as Java 8?s defender methods aren’t introduced yet. There are several techniques to circumvent / work around this problem:
1. Don’t care about it
Yes, that’s an option too. Your API is public, but maybe not so much used. Let’s face it: Not all of us work on the JDK / Eclipse / Apache / etc codebases. If you’re friendly, you’re at least going to wait for a major release to introduce new methods. But you can break the rules of semantic versioning if you really have to – if you can deal with the consequences of getting a mob of angry users.
Note, though, that other platforms aren’t as backwards-compatible as the Java universe (often by language design, or by language complexity). E.g. with Scala’s various ways of declaring things as implicit, your API can’t always be perfect.
2. Do it the Java way
The “Java” way is not to evolve interfaces at all. Most API types in the JDK have been the way they are today forever. Of course, this makes APIs feel quite “dinosaury” and adds a lot of redundancy between various similar types, such as StringBuffer and StringBuilder, or Hashtable and HashMap.
Note that some parts of Java don’t adhere to the “Java” way. Most specifically, this is the case for the JDBC API, which evolves according to the rules of section #1: “Don’t care about it”.
3. Do it the Eclipse way
Eclipse’s internals contain huge APIs. There are a lot of guidelines how to evolve your own APIs (i.e. public parts of your plugin), when developing for / within Eclipse. One example about how the Eclipse guys extend interfaces is the IAnnotationHover type. By Javadoc contract, it allows implementations to also implement IAnnotationHoverExtension and IAnnotationHoverExtension2. Obviously, in the long run, such an evolved API is quite hard to maintain, test, and document, and ultimately, hard to use! (consider ICompletionProposal and its 6 (!) extension types)
4. Wait for Java 8
In Java 8, you will be able to make use of defender methods. This means that you can provide a sensible default implementation for your new interface methods as can be seen in Java 1.8?s java.util.Iterator (an extract):
public interface Iterator<E> { // These methods are kept the same: boolean hasNext(); E next(); // This method is now made 'optional' (finally!) public default void remove() { throw new UnsupportedOperationException('remove'); } // This method has been added compatibly in Java 1.8 default void forEach(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); while (hasNext()) consumer.accept(next()); } }
Of course, you don’t always want to provide a default implementation. Often, your interface is a contract that has to be implemented entirely by client code.
5. Provide public default implementations
In many cases, it is wise to tell the client code that they may implement an interface at their own risk (due to API evolution), and they should better extend a supplied abstract or default implementation, instead. A good example for this is java.util.List, which can be a pain to implement correctly. For simple, not performance-critical custom lists, most users probably choose to extend java.util.AbstractList instead. The only methods left to implement are then get(int) and size(), The behaviour of all other methods can be derived from these two:
class EmptyList<E> extends AbstractList<E> { @Override public E get(int index) { throw new IndexOutOfBoundsException('No elements here'); } @Override public int size() { return 0; } }
A good convention to follow is to name your default implementation AbstractXXX if it is abstract, or DefaultXXX if it is concrete
6. Make your API very hard to implement
Now, this isn’t really a good technique, but just a probable fact. If your API is very hard to implement (you have 100s of methods in an interface), then users are probably not going to do it. Note: probably. Never underestimate the crazy user. An example of this is jOOQ’s org.jooq.Field type, which represents a database field / column. In fact, this type is part of jOOQ’s internal domain specific language, offering all sorts of operations and functions that can be performed upon a database column. Of course, having so many methods is an exception and – if you’re not designing a DSL – is probably a sign of a bad overall design.
7. Add compiler and IDE tricks
Last but not least, there are some nifty tricks that you can apply to your API, to help people understand what they ought to do in order to correctly implement your interface-based API. Here’s a tough example, that slaps the API designer’s intention straight into your face. Consider this extract of the org.hamcrest.Matcher API:
public interface Matcher<T> extends SelfDescribing { // This is what a Matcher really does. boolean matches(Object item); void describeMismatch(Object item, Description mismatchDescription); // Now check out this method here: /** * This method simply acts a friendly reminder not to implement * Matcher directly and instead extend BaseMatcher. It's easy to * ignore JavaDoc, but a bit harder to ignore compile errors . * * @see Matcher for reasons why. * @see BaseMatcher * @deprecated to make */ @Deprecated void _dont_implement_Matcher___instead_extend_BaseMatcher_(); }
“Friendly reminder”, come on.
Other ways
I’m sure there are dozens of other ways to evolve an interface-based API. I’m curious to hear your thoughts!
Reference: Defensive API evolution with Java interfaces from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog.