The document discusses best practices for designing Java APIs. It emphasizes making APIs intuitive, consistent, discoverable and easy to use. Some specific tips include using static factories to create objects, promoting fluent interfaces, using the weakest possible types, supporting lambdas, avoiding checked exceptions and properly managing resources using try-with-resources. The goal is to design APIs that are flexible, evolvable and minimize surprises for developers.
4. An API is what a
developer uses to
achieve some task
What is an API?
5. What is an API?
An API is a contract between
its implementors and its users
6. And why should I care?
We are all API designers
Our software doesn't work in
isolation, but becomes
useful only when it interacts
with other software written
by other developers
9. Be ready for changes
Software being 'done' is
like lawn being 'mowed'
– Jim Benson
Change is the only constant in software
Add features sparingly and carefully
so that they won’t become obstacles
for the evolution of your API
10. If in doubt, leave it out
A feature that only takes a few hours to
be implemented can
➢
create hundreds of hours of support
and maintenance in future
➢
bloat your software and confuse
your users
➢
become a burden and prevent
future improvements
“it’s easy to build” is NOT a good
enough reason to add it to your product
19. Convenience methods
public interface StockOrder {
void sell(String symbol, double price, int quantity);
void buy(String symbol, int quantity, double price);
void buy(String symbol, int quantity, double price, double commission);
void buy(String symbol, int quantity, double minPrice, double maxPrice, double commission);
}
What’s wrong with this?
20. Convenience methods
public interface StockOrder {
void sell(String symbol, double price, int quantity);
void buy(String symbol, int quantity, double price);
void buy(String symbol, int quantity, double price, double commission);
void buy(String symbol, int quantity, double minPrice, double maxPrice, double commission);
}
Too many overloads
What’s wrong with this?
21. Convenience methods
public interface StockOrder {
void sell(String symbol, double price, int quantity);
void buy(String symbol, int quantity, double price);
void buy(String symbol, int quantity, double price, double commission);
void buy(String symbol, int quantity, double minPrice, double maxPrice, double commission);
}
Too many overloads
Inconsistent argument order
What’s wrong with this?
22. Convenience methods
Long arguments lists (especially of same type)
public interface StockOrder {
void sell(String symbol, double price, int quantity);
void buy(String symbol, int quantity, double price);
void buy(String symbol, int quantity, double price, double commission);
void buy(String symbol, int quantity, double minPrice, double maxPrice, double commission);
}
Too many overloads
Inconsistent argument order
What’s wrong with this?
23. Convenience methods
Long arguments lists (especially of same type)
public interface StockOrder {
void sell(String symbol, double price, int quantity);
void buy(String symbol, int quantity, double price);
void buy(String symbol, int quantity, double price, double commission);
void buy(String symbol, int quantity, double minPrice, double maxPrice, double commission);
}
public interface StockOrder {
void sell(String symbol, int quantity, Price price);
void buy(String symbol, int quantity, Price price);
}
Too many overloads
Inconsistent argument order
What’s wrong with this?
How to do better
24. Consider static
factories
public interface Price {
static Price price( double price ) {
if (price < 0) return Malformed.INSTANCE;
return new Fixed(price);
}
static Price price( double minPrice, double maxPrice ) {
if (minPrice > maxPrice) return Malformed.INSTANCE;
return new Range(minPrice, maxPrice);
}
class Fixed implements Price {
private final double price;
private Fixed( double price ) {
this.price = price;
}
}
class Range implements Price {
private final double minPrice;
private final double maxPrice;
private Range( double minPrice, double maxPrice ) {
this.minPrice = minPrice;
this.maxPrice = maxPrice;
}
}
enum Malformed implements Price { INSTANCE }
}
➢
nicer syntax for users
(no need of new
keyword)
25. Consider static
factories
public interface Price {
static Price price( double price ) {
if (price < 0) return Malformed.INSTANCE;
return new Fixed(price);
}
static Price price( double minPrice, double maxPrice ) {
if (minPrice > maxPrice) return Malformed.INSTANCE;
return new Range(minPrice, maxPrice);
}
class Fixed implements Price {
private final double price;
private Fixed( double price ) {
this.price = price;
}
}
class Range implements Price {
private final double minPrice;
private final double maxPrice;
private Range( double minPrice, double maxPrice ) {
this.minPrice = minPrice;
this.maxPrice = maxPrice;
}
}
enum Malformed implements Price { INSTANCE }
}
➢
nicer syntax for users
(no need of new
keyword)
➢
can return different
subclasses
26. Consider static
factories
public interface Price {
static Price price( double price ) {
if (price < 0) return Malformed.INSTANCE;
return new Fixed(price);
}
static Price price( double minPrice, double maxPrice ) {
if (minPrice > maxPrice) return Malformed.INSTANCE;
return new Range(minPrice, maxPrice);
}
class Fixed implements Price {
private final double price;
private Fixed( double price ) {
this.price = price;
}
}
class Range implements Price {
private final double minPrice;
private final double maxPrice;
private Range( double minPrice, double maxPrice ) {
this.minPrice = minPrice;
this.maxPrice = maxPrice;
}
}
enum Malformed implements Price { INSTANCE }
}
➢
nicer syntax for users
(no need of new
keyword)
➢
can return different
subclasses
➢
can check
preconditions and
edge cases returning
different
implementations
accordingly
27. Promote fluent API
public interface Price {
Price withCommission(double commission);
Price gross();
}
public interface Price {
void setCommission(double commission);
void setGross();
}
32. Promote fluent API
Name consistency???
Streams are a very
nice and convenient
example of fluent API
33. Use the weakest possible type
public String concatenate( ArrayList<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
34. Use the weakest possible type
public String concatenate( ArrayList<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
Do I care of the actual
List implementation?
35. Use the weakest possible type
public String concatenate( List<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
36. Use the weakest possible type
public String concatenate( List<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
Do I care of the
elements’ order?
37. Use the weakest possible type
public String concatenate( Collection<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
38. Use the weakest possible type
public String concatenate( Collection<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
Do I care of the
Collection’s size?
39. Use the weakest possible type
public String concatenate( Iterable<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
40. Using the weakest possible type...
public String concatenate( Iterable<String> strings ) {
StringBuilder sb = new StringBuilder();
for (String s : strings) {
sb.append( s );
}
return sb.toString();
}
… enlarges the applicability of your method, avoiding to restrict your client
to a particular implementation or forcing it to perform an unnecessary and
potentially expensive copy operation if the input data exists in other forms
41. Use the weakest possible type
also for returned value
public List<Address> getFamilyAddresses( Person person ) {
List<Address> addresses = new ArrayList<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return addresses;
}
42. Use the weakest possible type
also for returned value
public List<Address> getFamilyAddresses( Person person ) {
List<Address> addresses = new ArrayList<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return addresses;
}
Is the order of this List
meaningful for client?
43. Use the weakest possible type
also for returned value
public List<Address> getFamilyAddresses( Person person ) {
List<Address> addresses = new ArrayList<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return addresses;
}
Is the order of this List
meaningful for client?
… and shouldn’t we maybe return only
the distinct addresses?
Yeah, that will be easy let’s do this!
44. Use the weakest possible type
also for returned value
public List<Address> getFamilyAddresses( Person person ) {
Set<Address> addresses = new HashSet<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return addresses;
}
It should be enough to
change this List into a Set
45. Use the weakest possible type
also for returned value
public List<Address> getFamilyAddresses( Person person ) {
Set<Address> addresses = new HashSet<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return addresses;
}
It should be enough to
change this List into a Set
But this doesn’t
compile :(
46. Use the weakest possible type
also for returned value
public List<Address> getFamilyAddresses( Person person ) {
Set<Address> addresses = new HashSet<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return addresses;
}
It should be enough to
change this List into a Set
But this doesn’t
compile :(
and I cannot change the returned type to
avoid breaking backward compatibility :(((
47. Use the weakest possible type
also for returned value
public List<Address> getFamilyAddresses( Person person ) {
Set<Address> addresses = new HashSet<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return new ArrayList<>( addresses );
}
I’m obliged to uselessly create an expensive
copy of data before returning them
48. Use the weakest possible type
also for returned value
public Collection<Address> getFamilyAddresses( Person person ) {
List<Address> addresses = new ArrayList<>();
addresses.add(person.getAddress());
for (Person sibling : person.getSiblings()) {
addresses.add(sibling.getAddress());
}
return addresses;
}
Returning a more generic type (if this is acceptable
for your client) provides better flexibility in future
49. Support lambdas
public interface Listener {
void beforeEvent(Event e);
void afterEvent(Event e);
}
class EventProducer {
public void registerListener(Listener listener) {
// register listener
}
}
public interface Listener {
void beforeEvent(Event e);
void afterEvent(Event e);
}
public interface Listener {
void beforeEvent(Event e);
void afterEvent(Event e);
}
EventProducer producer = new EventProducer();
producer.registerListener( new Listener() {
@Override
public void beforeEvent( Event e ) {
// ignore
}
@Override
public void afterEvent( Event e ) {
System.out.println(e);
}
} );
50. Support lambdas
class EventProducer {
public void registerBefore(BeforeListener before) {
// register listener
}
public void registerAfter(AfterListener after) {
// register listener
}
}
@FunctionalInterface
interface BeforeListener {
void beforeEvent( Event e );
}
@FunctionalInterface
interface AfterListener {
void afterEvent( Event e );
}
EventProducer producer = new EventProducer();
producer.registerAfter( System.out::println );
Taking functional interfaces as
argument of your API enables
clients to use lambdas
51. Support lambdas
class EventProducer {
public void registerBefore(Consumer<Event> before) {
// register listener
}
public void registerAfter(Consumer<Event> after) {
// register listener
}
}
@FunctionalInterface
interface BeforeListener {
void beforeEvent( Event e );
}
@FunctionalInterface
interface AfterListener {
void afterEvent( Event e );
}
EventProducer producer = new EventProducer();
producer.registerAfter( System.out::println );
Taking functional interfaces as
argument of your API enables
clients to use lambdas
In many cases you don’t need
to define your own functional
interfaces and use Java’s one
55. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
FileInputStream file = new FileInputStream( filename );
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
}
56. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
FileInputStream file = new FileInputStream( filename );
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
}
File descriptor leak
57. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
FileInputStream file = new FileInputStream( filename );
try {
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
} finally {
file.close();
}
}
58. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
FileInputStream file = new FileInputStream( filename );
try {
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
} finally {
file.close();
}
}
We can do better using
try-with-resource
59. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
try ( FileInputStream file = new FileInputStream( filename ) ) {
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
}
}
60. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
try ( FileInputStream file = new FileInputStream( filename ) ) {
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
}
}
Better, but we’re
still transferring
to our users the
burden to use
our API correctly
61. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
try ( FileInputStream file = new FileInputStream( filename ) ) {
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
}
}
Better, but we’re
still transferring
to our users the
burden to use
our API correctly
That’s a leaky abstraction!
62. Stay in control (loan pattern)
public static <T> T withFile( String filename,
ThrowingFunction<FileInputStream, T> consumer ) throws IOException {
try ( FileInputStream file = new FileInputStream( filename ) ) {
return consumer.apply( file );
}
}
@FunctionalInterface
public interface ThrowingFunction<T, R> {
R apply(T t) throws IOException;
}
Yeah, checked
exceptions
suck :(
63. Stay in control (loan pattern)
public byte[] readFile(String filename) throws IOException {
return withFile( filename, file -> {
byte[] buffer = new byte[4096];
ByteArrayOutputStream out = new ByteArrayOutputStream( buffer.length );
int n = 0;
while ( (n = file.read( buffer )) > 0 ) {
out.write( buffer, 0, n );
}
return out.toByteArray();
});
}
Now the
responsibility of
avoiding the leak
is encapsulated
in our API
If clients are
forced to use this
API no leak is
possible at all!
67. Be defensive with your data
public class Person {
private List<Person> siblings;
public List<Person> getSiblings() {
return siblings;
}
}
What’s the problem here?
68. public class Person {
private List<Person> siblings;
public List<Person> getSiblings() {
return siblings;
}
}
person.getSiblings().add(randomPerson);
What’s the problem here?
Be defensive with your data
69. public class Person {
private List<Person> siblings;
public List<Person> getSiblings() {
return siblings;
}
}
public class Person {
private List<Person> siblings;
public List<Person> getSiblings() {
return Collections.unmodifiableList( siblings );
}
}
If necessary return
unmodifiable objects to avoid
that a client could compromise
the consistency of your data.
person.getSiblings().add(randomPerson);
What’s the problem here?
Be defensive with your data
70. Return empty Collections or Optionals
public class Person {
private Car car;
private List<Person> siblings;
public Car getCar() {
return car;
}
public List<Person> getSiblings() {
return siblings;
}
}
What’s the problem here?
71. Return empty Collections or Optionals
public class Person {
private Car car;
private List<Person> siblings;
public Car getCar() {
return car;
}
public List<Person> getSiblings() {
return siblings;
}
}
What’s the problem here?
for (Person sibling : person.getSiblings()) { ... }
NPE!!!
72. Return empty Collections or Optionals
public class Person {
private Car car;
private List<Person> siblings;
public Car getCar() {
return car;
}
public List<Person> getSiblings() {
return siblings;
}
}
public class Person {
private Car car;
private List<Person> siblings;
public Optional<Car> getCar() {
return Optional.ofNullable(car);
}
public List<Person> getSiblings() {
return siblings == null ?
Collections.emptyList() :
Collections.unmodifiableList( siblings );
}
}
What’s the problem here?
for (Person sibling : person.getSiblings()) { ... }
NPE!!!
74. contacts.getPhoneNumber( ... );
Prefer enums to
boolean parameters
public interface EmployeeContacts {
String getPhoneNumber(boolean mobile);
}
Should I use true or false here?
75. contacts.getPhoneNumber( ... );
Prefer enums to
boolean parameters
public interface EmployeeContacts {
String getPhoneNumber(boolean mobile);
}
Should I use true or false here?
What if I may need to add a third
type of phone number in future?
78. Use meaningful
return types
public interface EmployeesRegistry {
enum PhoneType {
HOME, MOBILE, OFFICE;
}
Map<String, Map<PhoneType, List<String>>> getEmployeesPhoneNumbers();
}
Employee name
Employee’s
phone numbers
grouped by type
List of phone numbers
of a give type for a
given employee
Primitive
obsession
79. Use meaningful
return types
public interface EmployeesRegistry {
enum PhoneType {
HOME, MOBILE, OFFICE;
}
PhoneBook getPhoneBook();
}
public class PhoneBook {
private Map<String, EmployeeContacts> contacts;
public EmployeeContacts getEmployeeContacts(String name) {
return Optional.ofNullable( contacts.get(name) )
.orElse( EmptyContacts.INSTANCE );
}
}
public class EmployeeContacts {
private Map<PhoneType, List<String>> numbers;
public List<String> getNumbers(PhoneType type) {
return Optional.ofNullable( numbers.get(type) )
.orElse( emptyList() );
}
public static EmptyContacts INSTANCE = new EmptyContacts();
static class EmptyContacts extends EmployeeContacts {
@Override
public List<String> getNumbers(PhoneType type) {
return emptyList();
}
}
}
81. Optional – the mother of all bikeshedding
Principle of least
astonishment???
"If a necessary feature has a high
astonishment factor, it may be
necessary to redesign the feature."
- Cowlishaw, M. F. (1984). "The
design of the REXX language"
82. Optional – the mother of all bikeshedding
Principle of least
astonishment???
Wrong default
83. Optional – the mother of all bikeshedding
Principle of least
astonishment???
Wrong default
This could be removed if
the other was correctly
implemented
84. API design is an
iterative process
and there could
be different points
of view ...
85. … that could be
driven by the fact
that different
people may
weigh possible
use cases
differently...
86. … or even see
use cases to
which you didn’t
think at all
87. Also a good API
has many
different
characteristics ...
88. … and they
could be
conflicting so you
may need to
trade off one to
privilege another
89. What should
always drive the
final decision is
the intent of the
API … but even
there it could be
hard to find an
agreement
90. ●
Write lots of tests and examples against your API
●
Discuss it with colleagues and end users
●
Iterates multiple times to eliminate
➢
Unclear intentions
➢
Duplicated or redundant code
➢
Leaky abstraction
API design is an
iterative process
91. ●
Write lots of tests and examples against your API
●
Discuss it with colleagues and end users
●
Iterates multiple times to eliminate
➢
Unclear intentions
➢
Duplicated or redundant code
➢
Leaky abstraction
Practice Dogfeeding
API design is an
iterative process
92. And that’s all
what you were
getting wrong :)
… questions?
Mario Fusco
Red Hat – Principal Software Engineer
mario.fusco@gmail.com
twitter: @mariofusco