API Design
Tim BoudreauCampus Party, São Paulo 2013
http://timboudreau.com
API Design Is Software Design
● If you are coding, you are designing an API
– Even if it is only for yourself
● There is no API-style vs. non-API-style
– The same techniques that help you help your users
– Most people have learned many anti-patterns
● “The fast way” is not the enemy of “The Right Way”
What is an API
● Anything part of your code that somebody could call
– Method signatures, class signatures
● Anything someone can do that should affect the way your code runs
– Includes key names in property files, command-line argument names
● Anything which could break client if you change it
– Binary compatible – old compiled libraries will still work
– Source compatible – old libraries will still compile● Binary compatibility is more important than source compatibility
Why is API Design Important?
● Progress in software comes in the form of libraries
● Every revolutionary technological advance requires one thing:
– A technology so stable and solid people are not afraid to bet on it
● Bad practices stop people from writing stable, solid code
– So progress is slower – you are robbed of the ability to make really good things
– Everybody else is robbed of the benefits of your work
● Some bad habits are still taught as “the right way“
– People teach what they know
Practices that Work
● Have small interfaces
● Limit mutability
– JavaBeans are an anti-pattern
● Use small types to avoid big mistakes
● Prefer callbacks to locks
● Separate API and SPI
● final and not-public should be the default
● Write small libraries that do one thing well
● Use Java Generics to make your code more … generic!
Have Small Interfaces – Why?
● You get to reuse more code
– The more specific it is, the less reusable it is
● Small is beautiful :-)
– Humans understand small things easily
– You can still have complexity, but it comes from combining simple things
● Small is usable
– Think one- or two-method types
– If it has two methods and a good name, it's obvious what to do with it
● You will have an easier time keeping compatibility
● You will write better code
JDK Full of Bad Examples
● Even the parts that are called good: java.util.List – mixes many concerns:
– Array addressable by index
– Factory for iterators
– Thing which can be empty/not-empty
– Collection you can add to / remove from (mutable)
– Thing you can query
– Factory for arrays
● Mutability treated as the common case
● Painful to implement, usually you don't need most of it (ListIterator?!)
What is List, really?
public interface Bounded<N extends Number> { public N size();}public interface Keyed<K, T> { T get(K key);}public interface Indexed<N extends Number, T> extends Keyed<N, T> { T get(N index);}public interface KeyedQueryable<T, K> { K indexOf(T value);}public interface IndexedQueryable<T, N extends Number> extends KeyedQueryable<T, N> {}
What is List, really?
public interface Queryable<T> { boolean contains(T obj);}public interface Container { boolean isEmpty();}public interface MutableKeyed<N> { public void add(N key); public void remove(N key);}public class IndexedMutable<T, N extends Number> {}
What would that get you?
● A Map becomes a List with non-number keys
– Lots of code that works with Lists could work with Maps too● Better reuse – lots of things can be sorted – why is Collections.sort()
limited to java.util.List?public interface Sorter <K extends Comparable, T, R extends Keyed<K, T> & KeyedMutable<K,T> > {
boolean sort(R what);}
Limit Mutability – Why?
● It is the root of many (most?) bugs:
– Something changed when it should't have
– Threading bugs – something changed on the wrong thread
– Deadlocks because of locking to fix the threading bugs
– Liveness problems because of over-synchronization to fix the threading bugs
● Mutability leads to combinatoric explosions
● Mutability leads to verbose code
Limit Mutability with final
● Final is Java's secret weapon
– Let the compiler ensure your code is correct
– Limit the combinatorial explosion at construction-time
– Know that a field cannot, cannot, cannot change
– No synchronization, no threading problems
– Easy to debug – only one place a field can be set – obvious entry-points
– Your code runs faster – lots of optimizations possible on immutable data
● Too many constructor arguments? Use the builder pattern
Limit Mutability – Avoid the “Beans“ pattern
● JavaBeans were created for UI components
– Need to be mutable to change what the UI shows
– Completely wrong for modelling data unless it really must change
– If it must change, there are probably things that change together● Most of the time, replacing the whole data model is cheap
● Setters are evil
– If it should not change after construction time, don't make that possible
● Overridable setters are worse evil
– Can be called from super constructor, before object is initialized
Limit Mutability - Example
● Terrible problems with this:public class Bean { private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; }}● The value is null by default, and no guarantee that it won't be
– All users must check, or you assume your code is bug-free
● Too much text, too little utility
– Not thread-safe
– Verbose: Two words have meaning: “String value“. The rest is unnecessary noise
– It is not nice to humans, to make them write (or read) noise-code
Limit Mutability - Example
● This is actually worse:public class Bean { private String value; public String getValue() {...} public void setValue(String value) {…}
public boolean equals (Object o) { return o instanceof Bean ? Objects.equals(value, ((Bean) o).value) : false; }
public int hashCode() { return value == null ? 0 : value.hashCode(); }}● Now the object's identity can change on the fly
– In a Set, very bad things can happen
● hashCode() can throw a NullPointerException
– Another thread nulls value between the value==null test and value.hashCode()
Limit Mutability - Example
● But wait, it gets worse!public class Bean { private String value; public synchronized String getValue() { //this makes it thread-safe, right? return value; } public synchronized void setValue(String value) { this.value = value; } public boolean equals(){...} public int hashCode() {...} public String toString() { return getValue(); };}● toString() calls a synchronized method. Guess what can deadlock?
Logger.log (Level.FINE, “Look here {0}“, new Object[] { bean });
– This is not a theoretical problem! Just use JDK's (evil) FileLogger...
Limit Mutability - Example
Or you could make it as simple as it should be...
public class ImmutableBean { public final String value; //yes, public. Getters are not magic! public ImmutableBean(String value) { this.value = value; }}
...and the problems disappear
Limit Mutability - Combinatorics
● How many possible states does this code have?
public class Foo { public byte a, b;}
65536
Limit Mutability - Combinatorics
● And many possible states does this have?
public class Foo { public byte a, b, c, d;}
● This is much, much simpler than most application code
● Do you have 16777472 unit tests?
● Think twice about adding mutable state!
● final and a constructor would let you constrain and validate the state
● final would give you a single place this can change!
●Use small types to avoid big mistakes
● Use value-types to let compiler help you:
– Easy to mix up argument order:
new Location (double latitude, double longitude, double altitude);
– Impossible with this:
new Location (Latitude lat, Longitude lon, Altitude alt);
– Latitude, Longitude and Altitude can all implement Number if you want● And they should all be immutable :-)
Prefer Callbacks to Locks
● A free-for-all is not a threading model
● Using synchronized is not a threading model
– It is making threading someone else's problem
● What works:
– Define an interface
– Let people implement it and pass it to you
– Call it back on your own thread with whatever data you need to be thread-safe
– Protect thread-safe data with lock-checks
● If you just say “this mutable thing is thread-safe“, a lot more can go wrong
● Lock subsystems not individual methods
Prefer Callbacks to Locks - Example
● Just provide an interface for clients to do work insidepublic abstract class Receiver<T> { protected abstract void receive (T obj); protected void onCancel() { //do nothing - for subclasses }}
● And a way to get that interface calledpublic class FileIO { private ExecutorService ioThreads = …; public Canceller readFile(final File f, final Receiver<InputStream> r) { … }}
Separate API and SPI
● Most libraries have two faces
– API – the thing that clients call● Should be final classes or interfaces
– SPI – the thing that clients implement● Should be mostly abstract classes
● These should not touch each other
– A caller of the API should never directly touch SPI classes
– Most Java libraries get this completely wrong
Separate API and SPI – Why?
● You can compatibly add to API
● You can compatibly remove from SPI
– We are talking about binary compatibility here – more important than source-compatibility
● If a class is in the API and the SPI you cannot add or remove!
– Either it was perfect the first time, or you will have to break compatibility
– Nothing is perfect the first time :-)
Final, non-public as the default
● IDEs, Java classes teach you to put “public“ on Java classes. Don't do it!
● public = important
– If it is not useful to a caller, keep it out of the documentation and the API
● Humans can think about 5-6 things at the same time, maximum
● If everything is public, it is hard to see what is important vs. Implementation
● Everything that is public is API!
– You will have a hard time changing things compatibly● Bad for you, bad for your users
● So, make final and not-public the default, then choose what you will expose
Small Libraries that Do One Thing Well
● Flexibility does not come from software that does a lot of things
● Flexibility comes from being able to combine small things that work
– … into big things that work
● Less need for copy/paste programming
● You get to write new stuff faster
● You get to reuse your own code more
Use Java Generics to make code...Generic!
● When you are writing specific functionality
– Ask yourself if there is a more general pattern
– If yes, write that instead
public final class ConfigurationLoader {
public Configuration load() throws IOException {…}
}
● Becomespublic final class GenericLoader<T> {
public T load() throws IOException {… }
}
Thanks!
Tim BoudreauCampus Party, São Paulo 2013
http://timboudreau.com
Get the sample code here:
hg clone http://timboudreau.com/code/campusparty