SlideShare a Scribd company logo
Let's make a contract:
the art of designing a
Java API
by Mario Fusco
mario.fusco@gmail.com
@mariofusco
What is an API?
What is an API?
An API is what a
developer uses to
achieve some task
What is an API?
What is an API?
An API is a contract between
its implementors and its users
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
Basic Principles
●
Intuitive
●
Understandable
●
Learnable
●
Discoverable
●
Consistent
●
Self-defensive
●
Concise
●
Easy to use
●
Minimal
●
Orthogonal
●
Idiomatic
●
Flexible
●
Evolvable
●
Well documented
●
Right level of abstraction
●
Correct use of the type system
●
Limited number of entry-points
●
Respect the principle of least astonishment
Be ready for changes
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
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
Best Practices
&
Practical Hints
Write meaningful Javadocs
Write meaningful Javadocs
Write meaningful Javadocs
Write meaningful Javadocs
Convenience methods
Use overloading judiciously
and sparingly
Convenience methods
Use overloading judiciously
and sparingly
Primitives often cause
methods proliferation
{
Convenience methods
Use overloading judiciously
and sparingly
Are these all
necessary?
Primitives often cause
methods proliferation
{
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?
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?
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?
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?
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
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)
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
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
Promote fluent API
public interface Price {
Price withCommission(double commission);
Price gross();
}
public interface Price {
void setCommission(double commission);
void setGross();
}
Promote fluent API
public interface Price {
Price withCommission(double commission);
Price gross();
}
stockOrder.buy( "IBM", 100, price(150.0).withCommission(0.7).gross() );
public interface Price {
void setCommission(double commission);
void setGross();
}
Price price = price(150.0);
price.setCommission(0.7);
price.setGross();
stockOrder.buy( "IBM", 100, price );
Promote fluent API
public interface Price {
Price withCommission(double commission);
Price gross();
}
stockOrder.buy( "IBM", 100, price(150.0).withCommission(0.7).gross() );
public interface Price {
void setCommission(double commission);
void setGross();
}
Price price = price(150.0);
price.setCommission(0.7);
price.setGross();
stockOrder.buy( "IBM", 100, price );
Concatenate multiple invocations
Use result directly
Promote fluent API
Streams are a very
nice and convenient
example of fluent API
Promote fluent API
Streams are a very
nice and convenient
example of fluent API
Promote fluent API
Name consistency???
Streams are a very
nice and convenient
example of fluent API
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();
}
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?
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();
}
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?
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();
}
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?
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();
}
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
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;
}
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?
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!
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
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 :(
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 :(((
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
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
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);
}
} );
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
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
public void writeList( Writer writer, Collection<String> strings ) {
strings.stream().forEach( writer::write );
}
Writer writer = new StringWriter();
List<String> strings = asList("one", "two", "three");
writeList( writer, strings );
Avoid checked exceptions
Avoid checked exceptions
public void writeList( Writer writer, Collection<String> strings ) {
strings.stream().forEach( writer::write );
}
Writer writer = new StringWriter();
List<String> strings = asList("one", "two", "three");
writeList( writer, strings );
public void writeList( Writer writer, Collection<String> strings ) {
strings.stream().forEach( str -> {
try {
writer.write( str );
} catch (IOException e) {
throw new RuntimeException( e );
}
} );
}
Avoid checked exceptions
Useful error
management
or
Wasteful
bloatware
?
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();
}
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
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();
}
}
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
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();
}
}
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
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!
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 :(
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!
Break apart large interfaces into
smaller versions
public interface RuleEngineServices {
Resource newUrlResource( URL url );
Resource newByteArrayResource( byte[] bytes );
Resource newFileSystemResource( File file );
Resource newInputStreamResource( InputStream stream );
Resource newClassPathResource( String path );
void addModule( Module kModule );
Module getModule( ReleaseId releaseId );
Module removeModule( ReleaseId releaseId );
Command newInsert( Object object );
Command newModify( FactHandle factHandle );
Command newDelete( FactHandle factHandle );
Command newFireAllRules( int max );
RuntimeLogger newFileLogger( RuntimeEventManager session, String fileName, int maxEventsInMemory );
RuntimeLogger newThreadedFileLogger( RuntimeEventManager session, String fileName, int interval );
RuntimeLogger newConsoleLogger( RuntimeEventManager session );
}
Break apart large interfaces into
smaller versions
public interface RuleEngineServices {
Resources getResources();
Repository getRepository();
Loggers getLoggers();
Commands getCommands();
}
public interface Resources {
Resource newUrlResource( URL url );
Resource newByteArrayResource( byte[] bytes );
Resource newFileSystemResource( File file );
Resource newInputStreamResource( InputStream stream );
Resource newClassPathResource( String path );
}
public interface Repository {
void addModule( Module module );
Module getModule( ReleaseId releaseId );
Module removeModule( ReleaseId releaseId );
}
public interface Commands {
Command newInsert( Object object );
Command newModify( FactHandle factHandle );
Command newDelete( FactHandle factHandle );
Command newFireAllRules( int max );
}
Break apart large interfaces into
smaller versions
public interface RuleEngineServices {
Resources getResources();
Repository getRepository();
Loggers getLoggers();
Commands getCommands();
}
public interface Resources {
Resource newUrlResource( URL url );
Resource newByteArrayResource( byte[] bytes );
Resource newFileSystemResource( File file );
Resource newInputStreamResource( InputStream stream );
Resource newClassPathResource( String path );
}
public interface Repository {
void addModule( Module module );
Module getModule( ReleaseId releaseId );
Module removeModule( ReleaseId releaseId );
}
public interface Commands {
Command newInsert( Object object );
Command newModify( FactHandle factHandle );
Command newDelete( FactHandle factHandle );
Command newFireAllRules( int max );
}
Divide et Impera
Be defensive with your data
public class Person {
private List<Person> siblings;
public List<Person> getSiblings() {
return siblings;
}
}
What’s the problem here?
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
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
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?
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!!!
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!!!
contacts.getPhoneNumber( ... );
Prefer enums to
boolean parameters
public interface EmployeeContacts {
String getPhoneNumber(boolean mobile);
}
contacts.getPhoneNumber( ... );
Prefer enums to
boolean parameters
public interface EmployeeContacts {
String getPhoneNumber(boolean mobile);
}
Should I use true or false here?
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?
public interface EmployeeContacts {
String getPhoneNumber(PhoneType type);
enum PhoneType {
HOME, MOBILE, OFFICE;
}
}
Prefer enums to
boolean parameters
contacts.getPhoneNumber(PhoneType.HOME);
Use meaningful
return types
public interface EmployeesRegistry {
enum PhoneType {
HOME, MOBILE, OFFICE;
}
Map<String, Map<PhoneType, List<String>>> getEmployeesPhoneNumbers();
}
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
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();
}
}
}
Optional – the mother of all bikeshedding
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"
Optional – the mother of all bikeshedding
Principle of least
astonishment???
Wrong default
Optional – the mother of all bikeshedding
Principle of least
astonishment???
Wrong default
This could be removed if
the other was correctly
implemented
API design is an
iterative process
and there could
be different points
of view ...
… that could be
driven by the fact
that different
people may
weigh possible
use cases
differently...
… or even see
use cases to
which you didn’t
think at all
Also a good API
has many
different
characteristics ...
… and they
could be
conflicting so you
may need to
trade off one to
privilege another
What should
always drive the
final decision is
the intent of the
API … but even
there it could be
hard to find an
agreement
●
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
●
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
And that’s all
what you were
getting wrong :)
… questions?
Mario Fusco
Red Hat – Principal Software Engineer
mario.fusco@gmail.com
twitter: @mariofusco

More Related Content

PDF
If You Think You Can Stay Away from Functional Programming, You Are Wrong
PPTX
Python/Flask Presentation
PDF
Applicative style programming
PDF
Algebraic Data Types for Data Oriented Programming - From Haskell and Scala t...
PPTX
React render props
PDF
Idiomatic Kotlin
PDF
Introduction to Redux
PDF
Lazy java
If You Think You Can Stay Away from Functional Programming, You Are Wrong
Python/Flask Presentation
Applicative style programming
Algebraic Data Types for Data Oriented Programming - From Haskell and Scala t...
React render props
Idiomatic Kotlin
Introduction to Redux
Lazy java

What's hot (20)

PDF
Xpath in Selenium | Selenium Xpath Tutorial | Selenium Xpath Examples | Selen...
PDF
Domain Driven Design with the F# type System -- NDC London 2013
PPTX
Capabilities for Resources and Effects
PDF
Introduction to Rust
PDF
Evolving a Clean, Pragmatic Architecture - A Craftsman's Guide
PDF
JavaScript: Variables and Functions
PDF
ZIO-Direct - Functional Scala 2022
PPTX
Testing Spring Boot application in post-JUnit 4 world
PDF
Laziness, trampolines, monoids and other functional amenities: this is not yo...
PDF
Monadic Java
PDF
Java 8 Lambda Expressions
PDF
ES6 presentation
PDF
From object oriented to functional domain modeling
PDF
Java 8 Lambda Built-in Functional Interfaces
PPTX
The Art of Metaprogramming in Java
PPT
JUnit 4
PDF
Getting started with karate dsl
PPTX
Functional programming with Java 8
PPTX
jQuery
PDF
Map(), flatmap() and reduce() are your new best friends: simpler collections,...
Xpath in Selenium | Selenium Xpath Tutorial | Selenium Xpath Examples | Selen...
Domain Driven Design with the F# type System -- NDC London 2013
Capabilities for Resources and Effects
Introduction to Rust
Evolving a Clean, Pragmatic Architecture - A Craftsman's Guide
JavaScript: Variables and Functions
ZIO-Direct - Functional Scala 2022
Testing Spring Boot application in post-JUnit 4 world
Laziness, trampolines, monoids and other functional amenities: this is not yo...
Monadic Java
Java 8 Lambda Expressions
ES6 presentation
From object oriented to functional domain modeling
Java 8 Lambda Built-in Functional Interfaces
The Art of Metaprogramming in Java
JUnit 4
Getting started with karate dsl
Functional programming with Java 8
jQuery
Map(), flatmap() and reduce() are your new best friends: simpler collections,...
Ad

Similar to Let's make a contract: the art of designing a Java API (20)

PDF
Let's make a contract: The art of designing a Java API | DevNation Tech Talk
PPTX
Functional Programming with C#
PPTX
TypeScript Presentation - Jason Haffey
KEY
Domänenspezifische Sprachen mit Xtext
PDF
What's new in PHP 8.0?
PDF
Nikita Popov "What’s new in PHP 8.0?"
PPT
Effective Java - Enum and Annotations
DOCX
SeriesTester.classpathSeriesTester.project SeriesT.docx
KEY
Solid principles
PDF
Legacy is Good
PPTX
Is your C# optimized
PDF
An introduction to functional programming with Swift
PPTX
TypeScript by Howard
PPTX
Howard type script
PPTX
Type script by Howard
PDF
Transaction is a monad
PDF
FP in Java - Project Lambda and beyond
PPTX
Back-2-Basics: .NET Coding Standards For The Real World (2011)
PPT
Functional Programming In Java
PPTX
Java Static Factory Methods
Let's make a contract: The art of designing a Java API | DevNation Tech Talk
Functional Programming with C#
TypeScript Presentation - Jason Haffey
Domänenspezifische Sprachen mit Xtext
What's new in PHP 8.0?
Nikita Popov "What’s new in PHP 8.0?"
Effective Java - Enum and Annotations
SeriesTester.classpathSeriesTester.project SeriesT.docx
Solid principles
Legacy is Good
Is your C# optimized
An introduction to functional programming with Swift
TypeScript by Howard
Howard type script
Type script by Howard
Transaction is a monad
FP in Java - Project Lambda and beyond
Back-2-Basics: .NET Coding Standards For The Real World (2011)
Functional Programming In Java
Java Static Factory Methods
Ad

More from Mario Fusco (17)

PDF
Kogito: cloud native business automation
PDF
How and why I turned my old Java projects into a first-class serverless compo...
PDF
OOP and FP
ODP
Drools 6 deep dive
PDF
OOP and FP - Become a Better Programmer
PDF
Reactive Programming for a demanding world: building event-driven and respons...
PDF
Comparing different concurrency models on the JVM
PDF
Java 8 Workshop
PDF
Why we cannot ignore Functional Programming
PPTX
Real world DSL - making technical and business people speaking the same language
PDF
Introducing Drools
PPTX
Java 7, 8 & 9 - Moving the language forward
PDF
Hammurabi
PPT
Swiss army knife Spring
PDF
No more loops with lambdaj
PPTX
Concurrency, Scalability & Fault-tolerance 2.0 with Akka Actors & STM
PPTX
Scala - where objects and functions meet
Kogito: cloud native business automation
How and why I turned my old Java projects into a first-class serverless compo...
OOP and FP
Drools 6 deep dive
OOP and FP - Become a Better Programmer
Reactive Programming for a demanding world: building event-driven and respons...
Comparing different concurrency models on the JVM
Java 8 Workshop
Why we cannot ignore Functional Programming
Real world DSL - making technical and business people speaking the same language
Introducing Drools
Java 7, 8 & 9 - Moving the language forward
Hammurabi
Swiss army knife Spring
No more loops with lambdaj
Concurrency, Scalability & Fault-tolerance 2.0 with Akka Actors & STM
Scala - where objects and functions meet

Recently uploaded (20)

PPTX
history of c programming in notes for students .pptx
PDF
Raksha Bandhan Grocery Pricing Trends in India 2025.pdf
PDF
PTS Company Brochure 2025 (1).pdf.......
PDF
Understanding Forklifts - TECH EHS Solution
PDF
Nekopoi APK 2025 free lastest update
PDF
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
PDF
Why TechBuilder is the Future of Pickup and Delivery App Development (1).pdf
PDF
Design an Analysis of Algorithms II-SECS-1021-03
PDF
wealthsignaloriginal-com-DS-text-... (1).pdf
PDF
Design an Analysis of Algorithms I-SECS-1021-03
PDF
How Creative Agencies Leverage Project Management Software.pdf
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 41
PPTX
Essential Infomation Tech presentation.pptx
PPTX
CHAPTER 2 - PM Management and IT Context
PDF
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free
PPTX
ai tools demonstartion for schools and inter college
PDF
2025 Textile ERP Trends: SAP, Odoo & Oracle
PPTX
Odoo POS Development Services by CandidRoot Solutions
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PDF
System and Network Administraation Chapter 3
history of c programming in notes for students .pptx
Raksha Bandhan Grocery Pricing Trends in India 2025.pdf
PTS Company Brochure 2025 (1).pdf.......
Understanding Forklifts - TECH EHS Solution
Nekopoi APK 2025 free lastest update
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
Why TechBuilder is the Future of Pickup and Delivery App Development (1).pdf
Design an Analysis of Algorithms II-SECS-1021-03
wealthsignaloriginal-com-DS-text-... (1).pdf
Design an Analysis of Algorithms I-SECS-1021-03
How Creative Agencies Leverage Project Management Software.pdf
Internet Downloader Manager (IDM) Crack 6.42 Build 41
Essential Infomation Tech presentation.pptx
CHAPTER 2 - PM Management and IT Context
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free
ai tools demonstartion for schools and inter college
2025 Textile ERP Trends: SAP, Odoo & Oracle
Odoo POS Development Services by CandidRoot Solutions
Wondershare Filmora 15 Crack With Activation Key [2025
System and Network Administraation Chapter 3

Let's make a contract: the art of designing a Java API

  • 1. Let's make a contract: the art of designing a Java API by Mario Fusco mario.fusco@gmail.com @mariofusco
  • 2. What is an API?
  • 3. What is an API?
  • 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
  • 7. Basic Principles ● Intuitive ● Understandable ● Learnable ● Discoverable ● Consistent ● Self-defensive ● Concise ● Easy to use ● Minimal ● Orthogonal ● Idiomatic ● Flexible ● Evolvable ● Well documented ● Right level of abstraction ● Correct use of the type system ● Limited number of entry-points ● Respect the principle of least astonishment
  • 8. Be ready for changes
  • 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
  • 16. Convenience methods Use overloading judiciously and sparingly
  • 17. Convenience methods Use overloading judiciously and sparingly Primitives often cause methods proliferation {
  • 18. Convenience methods Use overloading judiciously and sparingly Are these all necessary? Primitives often cause methods proliferation {
  • 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(); }
  • 28. Promote fluent API public interface Price { Price withCommission(double commission); Price gross(); } stockOrder.buy( "IBM", 100, price(150.0).withCommission(0.7).gross() ); public interface Price { void setCommission(double commission); void setGross(); } Price price = price(150.0); price.setCommission(0.7); price.setGross(); stockOrder.buy( "IBM", 100, price );
  • 29. Promote fluent API public interface Price { Price withCommission(double commission); Price gross(); } stockOrder.buy( "IBM", 100, price(150.0).withCommission(0.7).gross() ); public interface Price { void setCommission(double commission); void setGross(); } Price price = price(150.0); price.setCommission(0.7); price.setGross(); stockOrder.buy( "IBM", 100, price ); Concatenate multiple invocations Use result directly
  • 30. Promote fluent API Streams are a very nice and convenient example of fluent API
  • 31. Promote fluent API Streams are a very nice and convenient example of fluent API
  • 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
  • 52. public void writeList( Writer writer, Collection<String> strings ) { strings.stream().forEach( writer::write ); } Writer writer = new StringWriter(); List<String> strings = asList("one", "two", "three"); writeList( writer, strings ); Avoid checked exceptions
  • 53. Avoid checked exceptions public void writeList( Writer writer, Collection<String> strings ) { strings.stream().forEach( writer::write ); } Writer writer = new StringWriter(); List<String> strings = asList("one", "two", "three"); writeList( writer, strings ); public void writeList( Writer writer, Collection<String> strings ) { strings.stream().forEach( str -> { try { writer.write( str ); } catch (IOException e) { throw new RuntimeException( e ); } } ); }
  • 54. Avoid checked exceptions Useful error management or Wasteful bloatware ?
  • 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!
  • 64. Break apart large interfaces into smaller versions public interface RuleEngineServices { Resource newUrlResource( URL url ); Resource newByteArrayResource( byte[] bytes ); Resource newFileSystemResource( File file ); Resource newInputStreamResource( InputStream stream ); Resource newClassPathResource( String path ); void addModule( Module kModule ); Module getModule( ReleaseId releaseId ); Module removeModule( ReleaseId releaseId ); Command newInsert( Object object ); Command newModify( FactHandle factHandle ); Command newDelete( FactHandle factHandle ); Command newFireAllRules( int max ); RuntimeLogger newFileLogger( RuntimeEventManager session, String fileName, int maxEventsInMemory ); RuntimeLogger newThreadedFileLogger( RuntimeEventManager session, String fileName, int interval ); RuntimeLogger newConsoleLogger( RuntimeEventManager session ); }
  • 65. Break apart large interfaces into smaller versions public interface RuleEngineServices { Resources getResources(); Repository getRepository(); Loggers getLoggers(); Commands getCommands(); } public interface Resources { Resource newUrlResource( URL url ); Resource newByteArrayResource( byte[] bytes ); Resource newFileSystemResource( File file ); Resource newInputStreamResource( InputStream stream ); Resource newClassPathResource( String path ); } public interface Repository { void addModule( Module module ); Module getModule( ReleaseId releaseId ); Module removeModule( ReleaseId releaseId ); } public interface Commands { Command newInsert( Object object ); Command newModify( FactHandle factHandle ); Command newDelete( FactHandle factHandle ); Command newFireAllRules( int max ); }
  • 66. Break apart large interfaces into smaller versions public interface RuleEngineServices { Resources getResources(); Repository getRepository(); Loggers getLoggers(); Commands getCommands(); } public interface Resources { Resource newUrlResource( URL url ); Resource newByteArrayResource( byte[] bytes ); Resource newFileSystemResource( File file ); Resource newInputStreamResource( InputStream stream ); Resource newClassPathResource( String path ); } public interface Repository { void addModule( Module module ); Module getModule( ReleaseId releaseId ); Module removeModule( ReleaseId releaseId ); } public interface Commands { Command newInsert( Object object ); Command newModify( FactHandle factHandle ); Command newDelete( FactHandle factHandle ); Command newFireAllRules( int max ); } Divide et Impera
  • 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!!!
  • 73. contacts.getPhoneNumber( ... ); Prefer enums to boolean parameters public interface EmployeeContacts { String getPhoneNumber(boolean mobile); }
  • 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?
  • 76. public interface EmployeeContacts { String getPhoneNumber(PhoneType type); enum PhoneType { HOME, MOBILE, OFFICE; } } Prefer enums to boolean parameters contacts.getPhoneNumber(PhoneType.HOME);
  • 77. Use meaningful return types public interface EmployeesRegistry { enum PhoneType { HOME, MOBILE, OFFICE; } Map<String, Map<PhoneType, List<String>>> getEmployeesPhoneNumbers(); }
  • 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(); } } }
  • 80. Optional – the mother of all bikeshedding
  • 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