718 words | 3 minutes to read
In the previous best practice, we covered the importance of minimizing API visibilty. This spoke to the importance of not leaking our implementation out into public API, and being very conscious of having a clear separation between API and implementation. In this best practice we shift our focus to instead questioning the value of (and need for) all of our public API.
The easiest API to maintain is no API at all, and it is therefore extremely important to require justification for every method and class that forms part of our API. During the process of designing our APIs, we should therefore frequently find ourselves asking the following question: ‘is this really required?’. We should assure ourselves that the API is paying for itself, returning vital functionality in return for its continued existence.
It is the natural instinct of an API developer to want to write as much API as they can - to offer more convenience rather than less - but this leads to two concerns:
All API developers should start by understanding the critical use cases required for their API, and design their API to support these use cases. They should fight the urge to add more convenience (thinking to themselves that by adding a new method will save developers from writing a few more lines of code).
Having said this, it is important to clarify that convenience APIs serve a critically important role in any good API, especially in servicing our goal of having an understandable API. The challenge is in determining what should be accepted as valuable, and what should be rejected as not having enough value to ‘pay for itself’. An example of a good convenience API in the JDK is the
List.add(Object) method, to avoid developers always having to call
When discussing this topic with Stuart Marks, an engineer on the JDK team at Oracle, he provided the following insight:
On the other hand I’ve seen APIs that really get bogged down in “conveniences”. Here’s a hypothetical example. Suppose you have an API that has
foo()operations. They are useful individually, but they are quite frequently used together. So you might have a
bf()operation that does both. OK so far.
Now suppose you add a
mumble()operation. It sort of sticks out to have to call
mumble()separately, so maybe you need more convenience APIs – like
bfm(). Well, what if you don’t need
foo()? How about
bm()? And throw in
fm()for good measure. Now you have seven methods, more than half of which are just combinations of the fundamental three operations. Maybe this is good, maybe not; it certainly has potential to bloat the APIs. At a certain point there are enough ‘convenience’ methods that they tend to outweigh the basic operations.
Now this is mainly a matter of style. You could just do the basic operations and let the user compose them. This is the JDK style. Or you could provide ALL the combinations, so that once the user knows they system, any combination they could possibly need is already there. For an example of the latter, see Eclipse Collections.
One pattern that has become more popular over time is the introduction of convenience classes. For example, in the JDK we have the
Collections class that has a bunch of useful utility methods that relate to the various collections types. Even more recently, with the introduction of static and default methods on interfaces, we’ve seen the introduction of static methods on popular interfaces such as
Stream, so that developers may simply write
List.of(x, y, z) and
Having a clear place where convenience can be found is a good strategy. Users can build up expectations of where to go looking for convenience, such as knowing it is always on the base interface types rather than concrete implementations. If there is no logical hierarchy to place the convenience API, creating a separate class, such as
Collections, is a good choice too.