https://dzone.com/refcardz/java-api-best-practices?chapter=2
3. Consistent
The reason why
https://tech.ticketmaster.com/2015/07/31/designing-an-api-that-developers-love/
PIE principle: Predictable, Intuitive and Efficient
http://www.artima.com/weblogs/viewpost.jsp?thread=142428
https://dzone.com/articles/how-design-good-regular-api
Regularity is what happens when you follow the “Principle of Least Astonishment“
Rule #1: Establish strong terms
http://tutorials.jenkov.com/api-design/index.html
---- ???
API Design: Don't Expose More than Necessary
The more of the internals of an API that is exposed to the user, the more the user needs to learn before she can master that API.
http://tutorials.jenkov.com/api-design/provide-sensible-defaults.html
By "defaults" is meant, that if a certain parameter value, interface implementation, or subclass is used most of the time, provide a method that doesn't take that parameter. Instead it should use that value internally. In other words, "hardcode" it.
In my experience though, as long as the dependencies do not have side effects, like requiring some JAR file to be present on the classpath which isn't already present, a dependency like the one shown here will most likely not cause any problems.
http://tutorials.jenkov.com/api-design/optional-abstractions.html
As mentioned earlier, each layer in a layered software model is only supposed to communicate with the layer just below, and just above itself. However, what often makes an API really flexible is the ability to bypass a layer and communicate directly with the lower layers. In other words, that the layers (abstractions) are optional. This is important to make sure that an abstraction (layer) does not "get in the users way",
http://tutorials.jenkov.com/api-design/central-point-of-access.html
http://tutorials.jenkov.com/api-design/dont-force-the-user-to-assemble-components.html
API Design: Don't Force the User to Assemble Components
Even if only a single class of an API is exposed to the outside world, you may still decide to split up the internal implementation into several smaller classes for various reasons.
http://tutorials.jenkov.com/api-design/design-for-testing.html
https://theamiableapi.com/java-cl-section-1-1/#cl.item.1.1.1
http://www.cs.bc.edu/~muller/teaching/cs102/s06/lib/pdf/api-design
Characteristics of a Good API
• Easy to learn
• Easy to use, even without documentation
• Hard to misuse
• Easy to read and maintain code that uses it
• Sufficiently powerful to satisfy requirements
• Easy to extend
• Appropriate to audience
Gather Requirements–with a Healthy
Degree of Skepticism
• Often you'll get proposed solutions instead
─ Better solutions may exist
• Your job is to extract true requirements
─ Should take the form of use-cases
• Can be easier and more rewarding to build
something more general
Start with Short Spec–1 Page is Ideal
• At this stage, agility trumps completeness
• Bounce spec off as many people as possible
─ Listen to their input and take it seriously
• If you keep the spec short, it’s easy to modify
• Flesh it out as you gain confidence
─ This necessarily involves coding
Write to Your API Early and Often
• Start before you've implemented the API
─ Saves you doing implementation you'll throw away
• Start before you've even specified it properly
─ Saves you from writing specs you'll throw away
• Continue writing to API as you flesh it out
─ Prevents nasty surprises
─ Code lives on as examples, unit tests
Writing to SPI is Even More Important
• Service Provider Interface (SPI)
─ Plug-in interface enabling multiple implementations
─ Example: Java Cryptography Extension (JCE)
• Write multiple plug-ins before release
─ If one, it probably won't support another
─ If two, it will support more with difficulty
─ If three, it will work fine
• Will Tracz calls this “The Rule of Threes”
(Confessions of a Used Program Salesman, Addison-Wesley, 1995)
Maintain Realistic Expectations
• Most API designs are over-constrained
─ You won't be able to please everyone
─ Aim to displease everyone equally
• Expect to make mistakes
─ A few years of real-world use will flush them out
─ Expect to evolve API
II. General Principles
API Should Do One Thing and Do it Well
• Functionality should be easy to explain
─ If it's hard to name, that's generally a bad sign
─ Good names drive development
─ Be amenable to splitting and merging modules
API Should Be As Small As Possible But
No Smaller
• API should satisfy its requirements
• When in doubt leave it out
─ Functionality, classes, methods, parameters, etc.
─ You can always add, but you can never remove
• Conceptual weight more important than bulk
• Look for a good power-to-weight ratio
Implementation Should Not Impact API
• Implementation details
─ Confuse users
─ Inhibit freedom to change implementation
• Be aware of what is an implementation detail
─ Do not overspecify the behavior of methods
─ For example: do not specify hash functions
─ All tuning parameters are suspect
• Don't let implementation details “leak” into API
─ On-disk and on-the-wire formats, exceptions
Minimize Accessibility of Everything
• Make classes and members as private as possible
• Public classes should have no public fields
(with the exception of constants)
• This maximizes information hiding
• Allows modules to be used, understood, built,
tested, and debugged independently
Names Matter–API is a Little Language
• Names Should Be Largely Self-Explanatory
─ Avoid cryptic abbreviations
• Be consistent–same word means same thing
─ Throughout API, (Across APIs on the platform)
• Be regular–strive for symmetry
• Code should read like prose
Documentation Matters
Reuse is something that is far easier to say than
to do. Doing it requires both good design and
very good documentation. Even when we see
good design, which is still infrequently, we won't
see the components reused without good
documentation
Document Religiously
• Document every class, interface, method,
constructor, parameter, and exception
─ Class: what an instance represents
─ Method: contract between method and its client
─ Preconditions, postconditions, side-effects
─ Parameter: indicate units, form, ownership
• Document state space very carefully
Consider Performance Consequences of
API Design Decisions
• Bad decisions can limit performance
─ Making type mutable
─ Providing constructor instead of static factory
─ Using implementation type instead of interface
• Do not warp API to gain performance
─ Underlying performance issue will get fixed,
but headaches will be with you forever
─ Good design usually coincides with good performance
Effects of API Design Decisions on
Performance are Real and Permanent
• Component.getSize() returns Dimension
• Dimension is mutable
• Each getSize call must allocate Dimension
• Causes millions of needless object allocations
• Alternative added in 1.2; old client code still slow - getWidth, getHeight
API Must Coexist Peacefully with Platform
• Do what is customary
─ Obey standard naming conventions
─ Avoid obsolete parameter and return types
─ Mimic patterns in core APIs and language
• Take advantage of API-friendly features
─ Generics, varargs, enums, default arguments
• Know and avoid API traps and pitfalls
─ Finalizers, public static final arrays
• Don’t Transliterate APIs
III. Class Design
Minimize Mutability
• Classes should be immutable unless there’s a
good reason to do otherwise
─ Advantages: simple, thread-safe, reusable
─ Disadvantage: separate object for each value
• If mutable, keep state-space small, well-defined
─ Make clear when it's legal to call which method
Bad: Date, Calendar
Good: TimerTask
Subclass Only Where It Makes Sense
• Subclassing implies substitutability (Liskov)
─ Subclass only when is-a relationship exists
─ Otherwise, use composition
• Public classes should not subclass other public
classes for ease of implementation
Bad: Properties extends Hashtable
Stack extends Vector
Good: Set extends Collection
Design and Document for Inheritance
or Else Prohibit it
• Inheritance violates encapsulation (Snyder, ‘86)
─ Subclass sensitive to implementation details of
superclass
• If you allow subclassing, document self-use
─ How do methods use one another?
• Conservative policy: all concrete classes final
Bad: Many concrete classes in J2SE libraries
Good: AbstractSet, AbstractMap
IV. Method Design
Don't Make the Client Do Anything the
Module Could Do
• Reduce need for boilerplate code
─ Generally done via cut-and-paste
─ Ugly, annoying, and error-prone
// DOM code to write an XML document to a specified output stream.
static final void writeDoc(Document doc, OutputStream out)throws IOException{
try {
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());
t.transform(new DOMSource(doc), new StreamResult(out));
} catch(TransformerException e) {
throw new AssertionError(e); // Can’t happen!
}
}
Don't Violate the Principle of Least
Astonishment
• User of API should not be surprised by behavior
─ It's worth extra implementation effort
─ It's even worth reduced performance
public class Thread implements Runnable {
// Tests whether current thread has been interrupted.
// Clears the interrupted status of current thread.
public static boolean interrupted();
}
Fail Fast–Report Errors as Soon as
Possible After They Occur
• Compile time is best - static typing, generics
• At runtime, first bad method invocation is best
─ Method should be failure-atomic
// A Properties instance maps strings to strings
public class Properties extends Hashtable {
public Object put(Object key, Object value);
// Throws ClassCastException if this properties
// contains any keys or values that are not strings
public void save(OutputStream out, String comments);
}
Provide Programmatic Access to All
Data Available in String Form
• Otherwise, clients will parse strings
─ Painful for clients
─ Worse, turns string format into de facto API
public class Throwable {
public void printStackTrace(PrintStream s);
public StackTraceElement[] getStackTrace(); // Since 1.4
}
Overload With Care
• Avoid ambiguous overloadings
─ Multiple overloadings applicable to same actuals
─ Conservative: no two with same number of args
• Just because you can doesn't mean you should
─ Often better to use a different name
• If you must provide ambiguous overloadings,
ensure same behavior for same arguments
public TreeSet(Collection c); // Ignores order
public TreeSet(SortedSet s); // Respects order
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
Use Appropriate Parameter and Return Types
• Favor interface types over classes for input
─ Provides flexibility, performance
• Use most specific possible input parameter type
─ Moves error from runtime to compile time
• Don't use string if a better type exists
─ Strings are cumbersome, error-prone, and slow
• Don't use floating point for monetary values
─ Binary floating point causes inexact results!
• Use double (64 bits) rather than float (32 bits)
─ Precision loss is real, performance loss negligible
Use Consistent Parameter Ordering
Across Methods
• Especially important if parameter types identical
#include <string.h>
char *strcpy (char *dest, char *src);
void bcopy (void *src, void *dst, int n);
java.util.Collections – first parameter always
collection to be modified or queried
java.util.concurrent – time always specified as
long delay, TimeUnit unit
Use Consistent Parameter Ordering
Across Methods
• Especially important if parameter types identical
#include <string.h>
char *strcpy (char *dest, char *src);
void bcopy (void *src, void *dst, int n);
java.util.Collections – first parameter always
collection to be modified or queried
java.util.concurrent – time always specified as
long delay, TimeUnit unit
Avoid Long Parameter Lists
• Three or fewer parameters is ideal
─ More and users will have to refer to docs
• Long lists of identically typed params harmful
─ Programmers transpose parameters by mistake
─ Programs still compile, run, but misbehave!
• Two techniques for shortening parameter lists
─ Break up method
─ Create helper class to hold parameters
// Eleven parameters including four consecutive ints
HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
LPVOID lpParam);
Avoid Return Values that Demand
Exceptional Processing
• return zero-length array or empty collection, not null
V. Exception Design
Throw Exceptions to Indicate
Exceptional Conditions
• Don’t force client to use exceptions for control flow
private byte[] a = new byte[BUF_SIZE];
void processBuffer (ByteBuffer buf) {
try {
while (true) {
buf.get(a);
processBytes(tmp, BUF_SIZE);
}
} catch (BufferUnderflowException e) {
int remaining = buf.remaining();
buf.get(a, 0, remaining);
processBytes(bufArray, remaining);
}
}
• Conversely, don’t fail silently
ThreadGroup.enumerate(Thread[] list)
Favor Unchecked Exceptions
• Checked – client must take recovery action
• Unchecked – programming error
• Overuse of checked exceptions causes boilerplate
try {
Foo f = (Foo) super.clone();
....
} catch (CloneNotSupportedException e) {
// This can't happen, since we’re Cloneable
throw new AssertionError();
}
Include Failure-Capture Information in
Exceptions
• Allows diagnosis and repair or recovery
• For unchecked exceptions, message suffices
• For checked exceptions, provide accessors
VI. Refactoring API Designs
• API design is a noble and rewarding craft
─ Improves the lot of programmers, end-users,
companies
• This talk covered some heuristics of the craft
─ Don't adhere to them slavishly, but...
─ Don't violate them without good reason
• API design is tough
─ Not a solitary activity
─ Perfection is unachievable, but try anyway
https://dzone.com/articles/the-java-8-api-design-principles
return Optional.ofNullable(comment);
Overloadding
https://dzone.com/articles/better-api-design-with-java-8-optional?fromrel=true
http://wiki.netbeans.org/API_Design
https://dzone.com/articles/designing-apis-with-customers-in-mind
Consider the perspective of the caller
Keep it simple
Strive for consistency
Choose memorable names
Specify the behaviour
Make it safe
Anticipate evolution
Write helpful documentation
Make it safe
Developers make mistakes
Prevent access to dangerous code
Keep implementation code private
Prevent class extension
Control class initialization
Prevent data corruption
Maximize compiler checks
Avoid out and in-out parameters
Check arguments at runtime
Provide informative error messages
Make method calls atomic
Write thread-safe code
Old API remains unchanged
must co-exist with the new API
Must be supported for years
often re-implemented as an Adaptor
Developers should give consideration to the entry points into their API. Complete documentation is useful and helps developers understand the bigger picture, but ideally, we want to ensure minimal friction for the developer, and therefore we should provide developers with the minimal steps to get started at the very top of our documentation. From here, good APIs try to expose their functionality through this entry point, to hold the developer’s hand as they make use of the primary use cases enabled by the API. More advanced functionality can then be discovered through external documentation as necessary.
During the process of designing our APIs, we should 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.
There are two final rules when it comes to returning an
Optional
in API:- Never return
Optional<Collection<T>>
from a method, as this can be more succinctly represented by simply returningCollection<T>
with an empty collection (as mentioned earlier in this refcard). - Never, ever return
null
from a method that has a return type ofOptional
protected
members are for communicating with subclasses, and public
members are for communicating with callers.
There are two main ways to hide implementation classes:
- Put implementation classes into packages under an
impl
package. All classes under this package can then be excluded from the JavaDoc output, and documented to not be part of the API for developers to use. If this API were being developed under JDK 9 or later, a module could be defined that excludes thisimpl
package from being exported (and therefore, it will not be available to developers at all). - Make implementation classes “package-private” (i.e. have no modifier on the class). This means that the classes are not part of the public API, and also are not able to be used by developers.
NON-NULL RETURN VALUE | |
---|---|
String | "" (An empty string) |
List / Set Map / Iterator | Use the Collections class, e.g. Collections.emptyList() |
Stream | Stream.empty() |
Array | Return an empty, zero-length array |
All other types | Consider using Optional (but read the Optional section below first) |
The reason why
final
is valuable to API developers is due to the fact that sometimes, our APIs are not flawless, and instead of getting in touch to help fix things, many developers want to try to work around our flaws to patch their version, so that they can move on to the next problem. With this approach, they only create new problems for themselves, and ultimately, for us as API developers. Ideally, when a user of our API encounters a final
class or method, they reach out to us to discuss their needs, which can lead to an even better API. The final
keyword, after all, can always be removed in a subsequent release, but it is a wise idea to not make something final
after it has already been releasedhttps://tech.ticketmaster.com/2015/07/31/designing-an-api-that-developers-love/
PIE principle: Predictable, Intuitive and Efficient
1. Predictable
They behave in a way that’s expected and do it in a consistent manner. No surprises. No Gotchas. Software is a repeatable process and a predictable API makes it easy to build software. Developers love that.
2. Intuitive
They have a simple and easy interface and deliver data that’s easy to understand. They are “as simple as possible, but not simpler,” to quote Einstein. This is critical for onboarding developers. If the API isn’t easy to use, they’ll move on to the competitor’s.
3. Efficient
They ask for the required input and deliver the expected output as fast as possible. Nothing more, nothing less.
/{localization}/{resource}/{version}/{identifiers}?[optional params]
localization: The market whose data we’re handling (i.e. us, eu, au, etc)
resource: The domain whose data we’re handling (i.e. artists, leagues, teams, venues, events, commerce, search, etc)
version: The version of the resource NOT the API.
identifiers: The required parameters needed to get a valid response from this API call
optional params: The optional parameters needed to filter or transform the response.
resource: The domain whose data we’re handling (i.e. artists, leagues, teams, venues, events, commerce, search, etc)
version: The version of the resource NOT the API.
identifiers: The required parameters needed to get a valid response from this API call
optional params: The optional parameters needed to filter or transform the response.
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.
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.Interfaces are overvalued
- Interfaces can be implemented by anybody. Suppose
String
were an interface. Then you could never be sure that aString
you got from somewhere obeyed the semantics you expect: it is immutable; itshashCode()
is computed in a certain way; its length is never negative; and so on. Code that usedString
, whether user code or code from the rest of the J2SE platform, would have to go to enormous lengths to ensure it was robust in the face ofString
implementations that were accidentally incorrect. And to even further lengths to ensure that its security could not be compromised by deliberately evilString
implementations.
In practice, implementations of APIs that are defined entirely in terms of interfaces often end up cheating and casting objects to the non-public implementation class. DOM typically does this for example. So you can't give your own implementation of theDocumentType
interface as a parameter toDOMImplementation.createDocument
and expect it to work. Then what's the point in having interfaces? - Interfaces cannot have constructors or static methods. If you need an instance of an interface, you either have to implement it yourself, or you have to ask some other object for it. If
Integer
were an interface, then to get theInteger
for a givenint
you could no longer use the obviousnew Integer(n)
(or, less obvious but still documented insideInteger
,Integer.valueOf(n)
). You would have to useIntegerFactory.newInteger(n)
or whatever. This makes your API harder to understand and use. - Interfaces cannot evolve. Suppose you add a new method to an interface in version 2 of your API. Then user code that implemented the interface in version 1 will no longer compile because it doesn't implement the new method. You can still preserve binary compatibility by catching
AbstractMethodError
around calls to the new method but that is clunky. If you use an abstract class instead of an interface you don't have this problem. If you tell users not to implement the interface then you don't have this problem either, but then why is it an interface? - Interfaces cannot be serialized. Java serialization has its problems, but you can't always get away from it. The JMX API relies heavily on serialization, for example. For better or worse, the way serialization works is that the name of the actual implementation class is serialized, and an instance of that exact same class is reconstructed at deserialization. If the implementation class is not a public class in your API, then you won't interoperate with other implementations of your API, and it will be very hard for you to ensure that you even interoperate between different versions of your own implementation. If the implementation class is a public class in your API, then do you really need the interface as well?
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 packagescom.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.
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.
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.
- Design to evolve.
- Correctness, then simplicity, then efficiency.
- Interfaces are overvalued.
- Be careful with packages.
- Read Effective Java.
Regularity is what happens when you follow the “Principle of Least Astonishment“
Rule #1: Establish strong terms
Apart from “feeling” like a horrible API (to me), here’s some more objective analysis:
- What’s the difference between a
Creator
and aFactory
- What’s the difference between a
Source
and aProvider
? - What’s the non-subtle difference between an
Advisor
and aProvider
? - What’s the non-subtle difference between a
Discoverer
and aProvider
? - Is an
Advisor
related to anAspectJAdvice
? - Is it a
ScanningCandidate
or aCandidateComponent
? - What’s a
TargetSource
? And how would it be different from aSourceTarget
if not aSourceSource
or my favourite: ASourceSourceTargetProviderSource
?
- It has
keySet()
and alsocontainsKey(Object)
- It has
values()
and alsocontainsValue(Object)
- It has
entrySet()
but nocontainsEntry(K, V)
Observe also, that there is no point of using the term
Set
in the method names. The method signature already indicates that the result has a Set
type. It would’ve been more consistent and symmetric if those methods would’ve been named keys()
, values()
, entries()
. (On a side-note, Sets
and Lists
are another topic that I will soon blog about, as I think those types do not pull their own weight either)
At the same time, the
Map
interface violates this rule by providingput(K, V)
and alsoputAll(Map)
remove(Object)
, but noremoveAll(Collection<?>)
Besides, establishing the term
clear()
instead of reusing removeAll()
with no arguments is unnecessary. This applies to all Collection API members. In fact, the clear()
method also violates rule #1. It is not immediately obvious, if clear
does anything subtly different from remove
when removing collection elements.Rule #3: Add convenience through overloading
There is mostly only one compelling reason, why you would want to overload a method: Convenience. Often you want to do precisely the same thing in different contexts, but constructing that very specific method argument type is cumbersome. So, for convenience, you offer your API users another variant of the same method, with a “friendlier” argument type set. This can be observed again in the
Collection
type. We have:toArray()
, which is a convenient overload of…toArray(T[])
- It has
keySet()
and alsocontainsKey(Object)
- It has
values()
and alsocontainsValue(Object)
- It has
entrySet()
but nocontainsEntry(K, V)
Observe also, that there is no point of using the term
Set
in the method names. The method signature already indicates that the result has a Set
type. It would’ve been more consistent and symmetric if those methods would’ve been named keys()
, values()
, entries()
. (On a side-note, Sets
and Lists
are another topic that I will soon blog about, as I think those types do not pull their own weight either)
At the same time, the
Map
interface violates this rule by providingput(K, V)
and alsoputAll(Map)
remove(Object)
, but noremoveAll(Collection<?>)
Besides, establishing the term
clear()
instead of reusing removeAll()
with no arguments is unnecessary. This applies to all Collection API members. In fact, the clear()
method also violates rule #1. It is not immediately obvious, if clear
does anything subtly different from remove
when removing collection elements.Rule #3: Add convenience through overloading
There is mostly only one compelling reason, why you would want to overload a method: Convenience. Often you want to do precisely the same thing in different contexts, but constructing that very specific method argument type is cumbersome. So, for convenience, you offer your API users another variant of the same method, with a “friendlier” argument type set. This can be observed again in the
Collection
type. We have:toArray()
, which is a convenient overload of…toArray(T[])
Overloading is mostly used for two reasons:
- Providing “default” argument behaviour, as in
Collection.toArray()
- Supporting several incompatible, yet “similar” argument sets, as in Arrays.copyOf()
Rule #4: Consistent argument ordering
Rule #5: Establish return value types
This may be a bit controversial as people may have different views on this topic. No matter what your opinion is, however, you should create a consistent, regular API when it comes to defining return value types. An example rule set (on which you may disagree):
- Methods returning a single object should return
null
when no object was found - Methods returning several objects should return an empty
List
,Set
,Map
, array, etc. when no object was found (nevernull
) - Methods should only throw exceptions in case of an … well, an exception
With such a rule set, it is not a good practice to have 1-2 methods lying around, which:
- … throw
ObjectNotFoundExceptions
when no object was found - … return
null
instead of emptyLists
NEVER return null when returning arrays or collections!
EntityManager.find()
methods returnnull
if no entity could be foundQuery.getSingleResult()
throws aNoResultException
if no entity could be found
As
NoResultException
is a RuntimeException
this flaw heavily violates the Principle of Least Astonishment, as you might stay unaware of this difference until runtime!http://tutorials.jenkov.com/api-design/index.html
Solve my problem, with minimal effort from me, and don't get in my way.
This means that the API should be:
- As easy to use as possible.
- As easy to learn as possible.
- As flexible as possible.
- Should actually solve users problem.
- Should not create new problems (e.g. by having annoying limitations)
The API becomes a part of somebody else's application. Change in the API may be cheap for you, but expensive for the users of your API.
Change is Expensive in the API's Public Interface
Some of the issues you might want to think about could be:
- How will the public interface look?
- How will the API be configured?
- What defaults should the API assume?
- Should any of the API's abstraction layers be optional?
Almost any API could have lots of little nice-to-have features added. Some of these features make the API easier to use, and are thus justified. Other features may seem like a good idea at first, but aren't really core features, or they only apply to a limited set of the total use cases within the domain they address. These should perhaps be left out. They may end up cluttering your API more than they improve it.
Avoid "Commons" APIs
It is tempting to try to isolate these utility methods and classes into a general purpose "Utility Library", so they can be reused from application to application. A bit like the Apache Commons library. But, I will advice you to think twice about doing that.
API Design: Don't Expose More than Necessary
The more of the internals of an API that is exposed to the user, the more the user needs to learn before she can master that API.
public class Crawler{ protected Indexer indexer = null; protected CrawlerListener listener = null; public Crawler(CrawlerListener listener){ this.indexer = new IndexerImpl(); this.listener = listener; } public Crawler(CrawlerListener listener, Indexer indexer){ this.indexer = indexer; this.listener = listener; } public void crawl(String url){ ... } }
Now the user can plugin an
Indexer
if she is up to the task of implementing one. The API is now exposing the Indexer
as well as the Crawler
, which isn't desirable most of the time. However, the user is still able to just ignore it, and use the first constructor. The user still doesn't need to know about the IndexerImpl
class.
The ability to plugin an
Indexer
could be useful during unit testing of the crawler. Plugging in a mock Indexer
would make it possible to test the Crawler
in isolation. This is not nearly as easy when it is not possible to plugin a mock Indexer
. So, this code now adheres to the tip Design for Testing. Note however, that this is a bit of a tradeoff between exposing as little as possible, and designing for testability. The world isn't perfect. Neither is software design.
Notice also how this code uses the tip Provide Sensible Defaults. The first constructor creates an instance internally of the default
Indexer
implementation IndexerImpl
.http://tutorials.jenkov.com/api-design/provide-sensible-defaults.html
By "defaults" is meant, that if a certain parameter value, interface implementation, or subclass is used most of the time, provide a method that doesn't take that parameter. Instead it should use that value internally. In other words, "hardcode" it.
Provide Default Dependencies
public class MyComponent{ protected MyDependency dependency = null; public MyComponent(){ this.dependency = new MyDefaultImpl(); } public MyComponent(MyDependency dependency){ this.dependency = dependency; } }
http://tutorials.jenkov.com/api-design/optional-abstractions.html
As mentioned earlier, each layer in a layered software model is only supposed to communicate with the layer just below, and just above itself. However, what often makes an API really flexible is the ability to bypass a layer and communicate directly with the lower layers. In other words, that the layers (abstractions) are optional. This is important to make sure that an abstraction (layer) does not "get in the users way",
For instance, if you do not need to read or write an object, nor read a
Map
, in Butterfly Persistence you can access the JDBC utilities directly instead, to do what you need to do. In fact, you can also bypass the JDBC utilities and work directly on the database connection if you need to. You can even combine the layers within the same transaction. For instance, you can open a ResultSet
and iterate it using the JDBC utilities, and then have the object reading layer read objects from records in the ResultSet
. That's how optional the layers in Butterfly Persistence are.
had to allow the flexibility of bypassing the automatic mapping. In fact, I made it possible to combine automatic and manual mapping, for increased ease of use and flexibility.
A third example is Butterfly DI Container, a dependency injection container I have designed. This too was designed for extreme flexibility. For instance, at any time if a certain object configuration gets too complicated to configure using the Butterfly Container Script, you can just write that as Java code in a static method (or normal method even). Then you can call this method from your script. This is an easy way of bypassing any limitations in the script language.
http://tutorials.jenkov.com/api-design/central-point-of-access.html
one of the goals of an API is to make it as easy as learn as possible. One way to make it easy to learn is if you keep the number of classes down that the user needs to know before she can use the API. A way to achieve this is to provide a central point of access to the API.
Factories as Central Point of Access
A factory class can be used as a central point of access. To do so, you would have a single factory from which you can access all objects of importance in the API. From each of the objects you obtain from this factory you should be able to access any objects not covered by the factory
Managers as Central Point of Access
In some API's it doesn't make sense to have a factory be the central point of access. Rather you want a class which is a combination of a factory and an object with some API behaviour.
Facades as Central Point of Access
Another way to provide a central point of access to an API is by providing a Facade (the design pattern) for the API. Rather than accessing all the classes of the API directly, the user will access the services provided by the API via this Facade class.
Providing a Facade can be handy if it is not possible or does not make sense to have a single central factory or manager class (well, a manager class can also be thought of as a kind of Facade). For instance, your API may have several different factories each responsible for creating part of the objects needed to perform the service the API provides. And, you might want to make it possible to replace factory implementations too. In that case it may not really make sense to have a central factory class.
Service Proxies as Central Point of Access
In a service oriented architecture (SOA) the central point of access for a service may be a service proxy. Jini uses intelligent service proxies as access point to Jini services, a SOAP client may auto-generate a service proxy from the WSDL of the service to access etc.
When you think about it, a service proxy is pretty similar to a Facade. So, whether you call your central point of access a service proxy or Facade isn't really that important. Use the name that fits best with your API and architecture.
API Design: Don't Force the User to Assemble Components
Even if only a single class of an API is exposed to the outside world, you may still decide to split up the internal implementation into several smaller classes for various reasons.
If you do force the user to assemble the whole hierarchy, the user will have to learn more details of your API than necessary. Even if you have a DI container inject all the instances, looking at the API docs may still confuse the user more than necessary.
Just to make sure you know what I mean, I've included a small code sample of A here:
public class A { protected B b = new B(); protected C c = new C(); protected D d = new D(); }http://tutorials.jenkov.com/api-design/avoid-external-dependencies.html
When you are implementing an API it may sometimes be a temptation to use external libraries, for instance the Apache Commons or Log4J, in your API.
Don't do it, unless there is absolutely no way around!
External dependencies make your API code swell quickly. Just look at Spring, or Apache Axis for proof of that. This means larger code bases, for the end user of the API, and thus sometimes slower build time. Slow build time can be really annoying during development, and a real time robber and productivity killer.
Additionally, the version of the external dependency you are using may clash with the version used in other API's, or in the final application your API is being used in.
External dependencies are, in my opinion, primarily for use in the final applications, not in API's and frameworks. Not unless you know for sure that the final application will also use the same version of that dependency. Or, if that dependency can be swapped for a different version without problems.
Don't Log Exceptions Either
You definately don't want to log any exceptions that occur inside your API either. Nor do you want to call the event listener with an exception. The user of your API is notified of exceptions by the thrown exception. The user of your API will then decide whether to log that exception, or propagate it up the call stack to be logged in a central place.
you should design for
- Testability of the API itself
- Testability of code that uses the API
Designing for Testability of the API
The easiest way to test code is typically via mock testing. This means that it should be easy to mock up the classes of your API, and easy to inject those mocks into the components you want to test.
To be able to inject mocks into the internals of your classes, you will unfortunately have to expose methods on the class interfaces that enable you to do so. For instance, either a constructor or setter method taking the mock to inject as parameter. To avoid exposing constructors or setters to users of the API, consider making these methods either package access scoped, or protected. If you put your test code in the same package (not necessarily same directory) as the class(es) you need to inject the mocks into, you will be able to access these extra injection methods.
Designing for Testability of Code Using the API
You are not the only one you need to take into consideration when designing your API for testability. Your users will most likely need to test the code that uses your API too. In that respect you should keep in mind that the users will need to create mock implementations of your publicly exposed API classes.
Use Interfaces
The easiest way to make your classes mockable is to have them implement an interface.
Use Extendable Classes
If you have not, or cannot have your classes implement interfaces, you should consider making the classes easy to subclass at least. That way a mock can be created by subclassing your API classes, and override the methods that need to be mocked / stubbed.
http://tutorials.jenkov.com/api-design/design-for-easy-configuration.html
Some of the most common API configuration mechanisms are:
- Method Calls on Components
- Annotations
- JVM Parameters
- Command Line Arguments
- Property Files
- XML Files
- A Domain Specific Language
Which of these configuration mechanisms is most appropriate for your API depends on several factors, like:
- How much configuration is needed?
- Is configuration an implementation choice or deployment choice?
- Is the configuration mechanism easy to learn, easy to use and concise?
- Which limitations does the configuration mechanism have?
Annotations, however, are class static. This means that it is not possible to have two different configurations of the same class. You can have only one. This is a serious limitation of annotations.
Similarly, ordinary property files may also impose some kind of limitations on your configuration options. For instance, it will be hard to configure hierarchical settings. For this purpose an XML file would be much more suitable.
https://theamiableapi.com/2012/01/16/java-api-design-checklist/https://theamiableapi.com/java-cl-section-1-1/#cl.item.1.1.1
1.1.1. Favor placing API and implementation into separate packages
package com.company.product; public class ApiClass { private ImplementationClass m; } package com.company.product.internal; public class ImplementationClass {...}
1.1.2. Favor placing APIs into high-level packages and implementation into lower-level packages
By convention, packages close to the base of this hierarchy are generic or more frequently used (ex. java.util) while deeper nested packages are more specialized or less frequently used (ex. java.util.concurrent.locks). API packages typically being the only public part of a component, service, or application are expected to be in the root namespace or as close as possible to the root namespace (package) reserved for the said component, service or application. Implementation packages should be at a lower level, preferably under the API package
1.1.8. Do not create dependencies between callers and implementation classes
1.2.9. Do not use uppercase characters in package names
1.2.6. Avoid using the same name for both package and class inside the package
Characteristics of a Good API
• Easy to learn
• Easy to use, even without documentation
• Hard to misuse
• Easy to read and maintain code that uses it
• Sufficiently powerful to satisfy requirements
• Easy to extend
• Appropriate to audience
Gather Requirements–with a Healthy
Degree of Skepticism
• Often you'll get proposed solutions instead
─ Better solutions may exist
• Your job is to extract true requirements
─ Should take the form of use-cases
• Can be easier and more rewarding to build
something more general
Start with Short Spec–1 Page is Ideal
• At this stage, agility trumps completeness
• Bounce spec off as many people as possible
─ Listen to their input and take it seriously
• If you keep the spec short, it’s easy to modify
• Flesh it out as you gain confidence
─ This necessarily involves coding
Write to Your API Early and Often
• Start before you've implemented the API
─ Saves you doing implementation you'll throw away
• Start before you've even specified it properly
─ Saves you from writing specs you'll throw away
• Continue writing to API as you flesh it out
─ Prevents nasty surprises
─ Code lives on as examples, unit tests
Writing to SPI is Even More Important
• Service Provider Interface (SPI)
─ Plug-in interface enabling multiple implementations
─ Example: Java Cryptography Extension (JCE)
• Write multiple plug-ins before release
─ If one, it probably won't support another
─ If two, it will support more with difficulty
─ If three, it will work fine
• Will Tracz calls this “The Rule of Threes”
(Confessions of a Used Program Salesman, Addison-Wesley, 1995)
Maintain Realistic Expectations
• Most API designs are over-constrained
─ You won't be able to please everyone
─ Aim to displease everyone equally
• Expect to make mistakes
─ A few years of real-world use will flush them out
─ Expect to evolve API
II. General Principles
API Should Do One Thing and Do it Well
• Functionality should be easy to explain
─ If it's hard to name, that's generally a bad sign
─ Good names drive development
─ Be amenable to splitting and merging modules
API Should Be As Small As Possible But
No Smaller
• API should satisfy its requirements
• When in doubt leave it out
─ Functionality, classes, methods, parameters, etc.
─ You can always add, but you can never remove
• Conceptual weight more important than bulk
• Look for a good power-to-weight ratio
Implementation Should Not Impact API
• Implementation details
─ Confuse users
─ Inhibit freedom to change implementation
• Be aware of what is an implementation detail
─ Do not overspecify the behavior of methods
─ For example: do not specify hash functions
─ All tuning parameters are suspect
• Don't let implementation details “leak” into API
─ On-disk and on-the-wire formats, exceptions
Minimize Accessibility of Everything
• Make classes and members as private as possible
• Public classes should have no public fields
(with the exception of constants)
• This maximizes information hiding
• Allows modules to be used, understood, built,
tested, and debugged independently
Names Matter–API is a Little Language
• Names Should Be Largely Self-Explanatory
─ Avoid cryptic abbreviations
• Be consistent–same word means same thing
─ Throughout API, (Across APIs on the platform)
• Be regular–strive for symmetry
• Code should read like prose
Documentation Matters
Reuse is something that is far easier to say than
to do. Doing it requires both good design and
very good documentation. Even when we see
good design, which is still infrequently, we won't
see the components reused without good
documentation
Document Religiously
• Document every class, interface, method,
constructor, parameter, and exception
─ Class: what an instance represents
─ Method: contract between method and its client
─ Preconditions, postconditions, side-effects
─ Parameter: indicate units, form, ownership
• Document state space very carefully
Consider Performance Consequences of
API Design Decisions
• Bad decisions can limit performance
─ Making type mutable
─ Providing constructor instead of static factory
─ Using implementation type instead of interface
• Do not warp API to gain performance
─ Underlying performance issue will get fixed,
but headaches will be with you forever
─ Good design usually coincides with good performance
Effects of API Design Decisions on
Performance are Real and Permanent
• Component.getSize() returns Dimension
• Dimension is mutable
• Each getSize call must allocate Dimension
• Causes millions of needless object allocations
• Alternative added in 1.2; old client code still slow - getWidth, getHeight
API Must Coexist Peacefully with Platform
• Do what is customary
─ Obey standard naming conventions
─ Avoid obsolete parameter and return types
─ Mimic patterns in core APIs and language
• Take advantage of API-friendly features
─ Generics, varargs, enums, default arguments
• Know and avoid API traps and pitfalls
─ Finalizers, public static final arrays
• Don’t Transliterate APIs
III. Class Design
Minimize Mutability
• Classes should be immutable unless there’s a
good reason to do otherwise
─ Advantages: simple, thread-safe, reusable
─ Disadvantage: separate object for each value
• If mutable, keep state-space small, well-defined
─ Make clear when it's legal to call which method
Bad: Date, Calendar
Good: TimerTask
Subclass Only Where It Makes Sense
• Subclassing implies substitutability (Liskov)
─ Subclass only when is-a relationship exists
─ Otherwise, use composition
• Public classes should not subclass other public
classes for ease of implementation
Bad: Properties extends Hashtable
Stack extends Vector
Good: Set extends Collection
Design and Document for Inheritance
or Else Prohibit it
• Inheritance violates encapsulation (Snyder, ‘86)
─ Subclass sensitive to implementation details of
superclass
• If you allow subclassing, document self-use
─ How do methods use one another?
• Conservative policy: all concrete classes final
Bad: Many concrete classes in J2SE libraries
Good: AbstractSet, AbstractMap
IV. Method Design
Don't Make the Client Do Anything the
Module Could Do
• Reduce need for boilerplate code
─ Generally done via cut-and-paste
─ Ugly, annoying, and error-prone
// DOM code to write an XML document to a specified output stream.
static final void writeDoc(Document doc, OutputStream out)throws IOException{
try {
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());
t.transform(new DOMSource(doc), new StreamResult(out));
} catch(TransformerException e) {
throw new AssertionError(e); // Can’t happen!
}
}
Don't Violate the Principle of Least
Astonishment
• User of API should not be surprised by behavior
─ It's worth extra implementation effort
─ It's even worth reduced performance
public class Thread implements Runnable {
// Tests whether current thread has been interrupted.
// Clears the interrupted status of current thread.
public static boolean interrupted();
}
Fail Fast–Report Errors as Soon as
Possible After They Occur
• Compile time is best - static typing, generics
• At runtime, first bad method invocation is best
─ Method should be failure-atomic
// A Properties instance maps strings to strings
public class Properties extends Hashtable {
public Object put(Object key, Object value);
// Throws ClassCastException if this properties
// contains any keys or values that are not strings
public void save(OutputStream out, String comments);
}
Provide Programmatic Access to All
Data Available in String Form
• Otherwise, clients will parse strings
─ Painful for clients
─ Worse, turns string format into de facto API
public class Throwable {
public void printStackTrace(PrintStream s);
public StackTraceElement[] getStackTrace(); // Since 1.4
}
Overload With Care
• Avoid ambiguous overloadings
─ Multiple overloadings applicable to same actuals
─ Conservative: no two with same number of args
• Just because you can doesn't mean you should
─ Often better to use a different name
• If you must provide ambiguous overloadings,
ensure same behavior for same arguments
public TreeSet(Collection c); // Ignores order
public TreeSet(SortedSet s); // Respects order
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
Use Appropriate Parameter and Return Types
• Favor interface types over classes for input
─ Provides flexibility, performance
• Use most specific possible input parameter type
─ Moves error from runtime to compile time
• Don't use string if a better type exists
─ Strings are cumbersome, error-prone, and slow
• Don't use floating point for monetary values
─ Binary floating point causes inexact results!
• Use double (64 bits) rather than float (32 bits)
─ Precision loss is real, performance loss negligible
Use Consistent Parameter Ordering
Across Methods
• Especially important if parameter types identical
#include <string.h>
char *strcpy (char *dest, char *src);
void bcopy (void *src, void *dst, int n);
java.util.Collections – first parameter always
collection to be modified or queried
java.util.concurrent – time always specified as
long delay, TimeUnit unit
Use Consistent Parameter Ordering
Across Methods
• Especially important if parameter types identical
#include <string.h>
char *strcpy (char *dest, char *src);
void bcopy (void *src, void *dst, int n);
java.util.Collections – first parameter always
collection to be modified or queried
java.util.concurrent – time always specified as
long delay, TimeUnit unit
Avoid Long Parameter Lists
• Three or fewer parameters is ideal
─ More and users will have to refer to docs
• Long lists of identically typed params harmful
─ Programmers transpose parameters by mistake
─ Programs still compile, run, but misbehave!
• Two techniques for shortening parameter lists
─ Break up method
─ Create helper class to hold parameters
// Eleven parameters including four consecutive ints
HWND CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName,
DWORD dwStyle, int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
LPVOID lpParam);
Avoid Return Values that Demand
Exceptional Processing
• return zero-length array or empty collection, not null
V. Exception Design
Throw Exceptions to Indicate
Exceptional Conditions
• Don’t force client to use exceptions for control flow
private byte[] a = new byte[BUF_SIZE];
void processBuffer (ByteBuffer buf) {
try {
while (true) {
buf.get(a);
processBytes(tmp, BUF_SIZE);
}
} catch (BufferUnderflowException e) {
int remaining = buf.remaining();
buf.get(a, 0, remaining);
processBytes(bufArray, remaining);
}
}
• Conversely, don’t fail silently
ThreadGroup.enumerate(Thread[] list)
Favor Unchecked Exceptions
• Checked – client must take recovery action
• Unchecked – programming error
• Overuse of checked exceptions causes boilerplate
try {
Foo f = (Foo) super.clone();
....
} catch (CloneNotSupportedException e) {
// This can't happen, since we’re Cloneable
throw new AssertionError();
}
Include Failure-Capture Information in
Exceptions
• Allows diagnosis and repair or recovery
• For unchecked exceptions, message suffices
• For checked exceptions, provide accessors
VI. Refactoring API Designs
• API design is a noble and rewarding craft
─ Improves the lot of programmers, end-users,
companies
• This talk covered some heuristics of the craft
─ Don't adhere to them slavishly, but...
─ Don't violate them without good reason
• API design is tough
─ Not a solitary activity
─ Perfection is unachievable, but try anyway
https://dzone.com/articles/the-java-8-api-design-principles
Getting it right from the start is important because once an API is published, a firm commitment is made to the people who are supposed to use it. As Joshua Bloch once said: “Public APIs, like diamonds, are forever. You have one chance to get it right, so give it your best.” A well-designed
API combines the best of two worlds, a firm and precise commitment combined with a high degree of implementation flexibility, eventually benefiting both the API designers and the API users.
API designers are strongly encouraged to put themselves in the client code perspective and to optimize that view in terms of simplicity, ease-of-use, and consistency — rather than thinking about the actual API implementation. At the same time, they should try to hide as many implementation details as possible.
Do not Return Null to Indicate the Absence of a Value
Arguably, inconsistent null handling (resulting in the ubiquitous NullPointerException) is the single largest source of Java applications’ errors historically.
Do not Use Arrays to Pass Values to and From the API
A significant API mistake was made when the Enum concept was introduced in Java 5. We all know that an Enum class has a method called values() that returns an array of all the Enum’s distinct values. Now, because the Java framework must ensure that the client code cannot change the Enum’s values (for example, by directly writing to the array), a copy of the internal array must be produced for each call to the value() method.
This results in poor performance and also poor client code usability. If the Enum would have returned an unmodifiable List, that List could be reused for each call and the client code would have had access to a better and more useful model of the Enum’s values. In the general case, consider exposing a Stream, if the API is to return a collection of elements. This clearly states that the result is read-only (as opposed to a List which has a set() method).
It also allows the client code to easily collect the elements in another data structure or act on them on-the-fly. Furthermore, the API can lazily produce the elements as they become available (e.g. are pulled in from a file, a socket, or from a database). Again, Java 8’s improved escape analysis will make sure that a minimum of objects are actually created on the Java heap.
Do not use arrays as input parameters for methods either, since this — unless a defensive copy of the array is made — makes it possible for another thread to modify the content of the array during method execution.
Consider Adding Static Interface Methods to Provide a Single Entry Point for Object Creation
Avoid allowing the client code to directly select an implementation class of an interface. Allowing client code to create implementation classes directly creates a much more direct coupling of the API and the client code. It also makes the API commitment much larger, since now we have to maintain all the implementation classes exactly as they can be observed from outside instead of just committing to the interface as such.
Consider adding static interface methods, to allow the client code to create (potentially specialized) objects that implement the interface. For example, if we have an interface Point with two methods int x() and int y(), then we can expose a static method Point.of(int x, int y) that produces a (hidden) implementation of the interface.
So, if x and y are both zero, we can return a special implementation class PointOrigoImpl (with no x or y fields), or else we return another class PointImpl that holds the given x and y values. Ensure that the implementation classes are in another package that are clearly not a part of the API (e.g. put the Point interface in com.company. product.shape and the implementations in com.company.product.internal.shape).
Do This:
Don't Do This:
Favor Composition With Functional Interfaces and Lambdas Over Inheritence
For good reasons, there can only be one super class for any given Java class. Furthermore, exposing abstract or base classes in your API that are supposed to be inherited by client code is a very big and problematic API commitment. Avoid API inheritance altogether, and instead consider providing static interface methods that take one or several lambda parameters and apply those given lambdas to a default internal API implementation class.
This also creates a much clearer separation of concerns. For example, instead of inheriting from a public API class AbstractReader and overriding abstract void handleError(IOException ioe), it is better to expose a static method or a builder in the Reader interface that takes a Consumer<IOException> and applies it to an internal generic ReaderImpl.
Do This:
Don't Do This:
Ensure That You Add the @FunctionalInterface Annotation to Functional Interfaces
Tagging an interface with the @FunctionalInterface annotation signals that API users may use lambdas to implement the interface, and it also makes sure the interface remains usable for lambdas over time by preventing abstract methods from accidently being added to the API later on.
Avoid Overloading Methods With Functional Interfaces as Parameters
If there are two or more functions with the same name that take functional interfaces as parameters, then this would likely create a lambda ambiguity on the client side. For example, if there are two Point methods add(Function<Point, String> renderer) and add(Predicate<Point> logCondition) and we try to call point.add(p -> p + “ lambda”) from the client code, the compiler is unable to determine which method to use and will produce an error. Instead, consider naming methods according to their specific use.
Do This:
Avoid Overusing Default Methods in Interfaces
Default methods can easily be added to interfaces and sometimes it makes sense to do that. For example, a method that is expected to be the same for any implementing class and that is short and “fundamental” in its functionality, is a viable candidate for a default implementation. Also, when an API is expanded, it sometimes makes sense to provide a default interface method for backward compatibility reasons.
As we all know, functional interfaces contain exactly one abstract method, so default methods provide an escape hatch when additional methods must be added. However, avoid having the API interface evolve to an implementation class by polluting it with unnecessary implementation concerns. If in doubt, consider moving the method logic to a separate utility class and/or place it in the implementing classes.
Ensure That the API Methods Check the Parameter Invariants Before They Are Acted Upon
Historically, people have been sloppy in making sure to validate method input parameters. So, when a resulting error occurs later on, the real reason becomes obscured and hidden deep down the stack trace. Ensure that parameters are checked for nulls and any valid range constrains or preconditions before the parameters are ever used in the implementing classes. Do not fall for the temptation to skip parameter checks for performance reasons.
The JVM will be able to optimize away redundant checking and produce efficient code. Make use of the Objects.requireNonNull() method. Parameter checking is also an important way to enforce the API’s contract. If the API was not supposed to accept nulls but did anyhow, users will become confused.
Do not Simply Call Optional.get()
The API designers of Java 8 made a mistake when they selected the name Optional.get() when it should really have been named Optional.getOrThrow() or something similar instead. Calling get() without checking if a value is present with the Optional.isPresent() method is a very common mistake which fully negates the null elimination features Optional originally promised. Consider using any of the Optional’s other methods such as map(), flatMap() or ifPresent() instead in the API’s implementing classes or ensure that isPresent() is called before any get() is called.
Consider Separating Your Stream Pipeline on Distinct Lines in Implementing API Classes
Eventually, all APIs will contain errors. When receiving stack traces from API users, it is often much easier to determine the actual cause of the error if a Stream pipeline is split into distinct lines compared to a Stream pipeline that is expressed on a single line. Also, code readability will improve.
Do This:
… you should now think about whether your method arguments are better modelled as functions for lazy evaluation:
1
2
3
4
5
6
7
8
9
10
| // Keep the existing method for convenience // and for backwards compatibility void performAction(Parameter parameter); // Overload the existing method with the new // functional one: void performAction(Supplier<Parameter> parameter); // Call the above: object.performAction(() -> new Parameter(...));
|
… you should stay wary when overloading “more similar” methods, like these ones:
1
2
| void performAction(Supplier<Parameter> parameter); void performAction(Callable<Parameter> parameter); |
If you produce the above API, your API’s client code will not be able to make use of lambda expressions, as there is no way of disambiguating a lambda that is a
https://blog.jooq.org/2014/04/04/java-8-friday-the-dark-side-of-java-8/Supplier
from a lambda that is a Callable
With lambda expressions, things get “worse”. So you think you can provide some convenience API, overloading your existing
run()
method that accepts a Callable
to also accept the new Supplier
type:
1
2
3
4
5
6
7
| static <T> T run(Callable<T> c) throws Exception { return c.call(); } static <T> T run(Supplier<T> s) throws Exception { return s.get(); } |
What looks like perfectly useful Java 7 code is a major pain in Java 8, now. Because you cannot just simply call these methods with a lambda argument:
1
2
3
4
5
| public static void main(String[] args) throws Exception { run(() -> null ); // ^^^^^^^^^^ ambiguous method call } |
Tough luck. You’ll have to resort to either of these “classic” solutions:
1
2
3
4
5
6
7
| run((Callable<Object>) (() -> null )); run( new Callable<Object>() { @Override public Object call() throws Exception { return null ; } }); |
So, while there’s always a workaround, these workarounds always “suck”. That’s quite a bummer, even if things don’t break from a backwards-compatibility perspective.
Let’s remember how the IOExceptions caused issues when traversing the file system. Unless you write your own utility, you’ll have to resort to this beauty:
1
2
3
4
5
6
7
8
9
10
| Arrays.stream(dir.listFiles()).forEach(file -> { try { System.out.println(file.getCanonicalPath()); } catch (IOException e) { throw new RuntimeException(e); } // Ouch, my fingers hurt! All this typing! }); |
We think it is safe to say:
https://blog.jooq.org/2017/03/17/a-nice-api-design-gem-strategy-pattern-with-lambdas/
If you have an interface of the form:
1
2
3
4
5
6
| interface MyInterface { void myMethod1(); String myMethod2(); void myMethod3(String value); String myMethod4(String value); } |
Then, just add a convenience constructor to the interface, accepting Java 8 functional interfaces like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // You write this boring stuff interface MyInterface { static MyInterface of( Runnable function1, Supplier<String> function2, Consumer<String> function3, Function<String, String> function4 ) { return new MyInterface() { @Override public void myMethod1() { function1.run(); } @Override public String myMethod2() { return function2.get(); } @Override public void myMethod3(String value) { function3.accept(value); } @Override public String myMethod4(String value) { return function4.apply(value); } } } } |
As an API designer, you write this boilerplate only once. And your users can then easily write things like these:
1
2
3
4
5
6
7
| // Your users write this awesome stuff MyInterface.of( () -> { ... }, () -> "hello" , v -> { ... }, v -> "world" ); |
public class Vehicle {
private Optional<MusicSystem> musicSystem;
public Optional<MusicSystem> getMusicSystem() {
return musicSystem;
}
publicvoid setMusicSystem(Optional<MusicSystem> musicSystem) {
this.musicSystem = musicSystem;
}
}
http://wiki.netbeans.org/API_Design
https://dzone.com/articles/designing-apis-with-customers-in-mind
These two failings shared a common theme: They were both failures of design, not of execution. Both the tooling and service developers fell into the trap of “inside-out” design. They built solutions based on what they wanted to give people, not what they wanted people to be able to accomplish with them. The services were shaped around the developers’ internal data models and the tools were built as thin wrappers around technologies.
- Write the docs first. This puts you in the mindset of a user instead of an author. It’s also much cheaper to argue about and change documentation than it is to discover your usability problems after the implementation is done.
- Try your APIs early by just writing client code that isn’t hooked up to anything, or by using a simple server to stub the API.
- What problem is the user trying to solve?
- How will they use “the product” to solve it?
- Test your assumptions with real users.
- Run UX experiments. Here is where internal tools and services have a huge advantage over externally facing products. Your users work at the same company, so go watch them. Ask them to think out loud while they are working and see how they attempt to interact with your API. Resist the urge to help them. Your users aren’t stupid, so if they can’t figure it out on their own, your job isn’t done.
- Give people who use your services or tools an easy way to provide feedback. No matter how open your organization is, if you don’t reach out and open an explicit channel for feedback, people will wait until they have a big problem before they bring up any issues.
Consider the perspective of the caller
Keep it simple
Strive for consistency
Choose memorable names
Specify the behaviour
Make it safe
Anticipate evolution
Write helpful documentation
Make it safe
Developers make mistakes
Prevent access to dangerous code
Keep implementation code private
Prevent class extension
Control class initialization
Prevent data corruption
Maximize compiler checks
Avoid out and in-out parameters
Check arguments at runtime
Provide informative error messages
Make method calls atomic
Write thread-safe code
Old API remains unchanged
must co-exist with the new API
Must be supported for years
often re-implemented as an Adaptor