A fluent (programming) interface is one that is used by chaining method calls.
Suppose we need to create a tool constructing and printing English sentences. Here are some possible programming interfaces to that tool:
Methods:
[java]public void printSentence(Person subject Action action, Object object, Time when);
// used as:
printSentence(john, Action.eat, Fruit.apple, Time.morning);
printSentence(mary, Action.sing, Song.jingleBells, /*Unspecified time */null);[/java]
Setters:
[java]Sentence sentence = new Sentence();
sentence.setSubject(john);
sentence.setAction(Action.eat);
sentence.setObject(Fruit.apple);
sentence.setTime(Time.morning);
sentence.print();[/java]
Inheritance:
[java]new AbstractSentence() {
public Person getSubject() { return mary; }
public Action getAction() { return Action.sing; }
public Object getObject() { return Song.jingleBells; }
/* getTime() not overriden → unspecified time */
].print();[/java]
Fluent:
[java]subject(mary)
.does(Action.sing)
.on(Song.jingleBells)
.print();
[/java]
These programming styles all have advantages and disadvantages but the topic of this post is not to help you choose between them. Suppose we have already chosen to implement a fluent interface, how should it be defined to be Level One against incorrect use?
The next sections will examine several requirements that qualify what “incorrect use” means.
Required Method Call
“The does
method must be called before invoking print
”
Level Six: set a boolean flag when does
is called, and check it is true when print
is called.
[java]public class SentenceBuilder {
private boolean doesCalled = false;
public SentenceBuilder does(Action action) {
doesCalled = true;
// …
return this;
}
public void print() {
Preconditions.checkState(doesCalled, “You must call does() before invoking print()”);
// …
}
}
// example use:
subject(john).action(Action.run).print();
// example misuse:
subject(john).print(); // ← throws an IllegalStateException
[/java]
Level One: Put the print
method in a (different) class returned by does
.
[java]public class SentenceBuilder {
Person subject;
Action action;
public static Subject subject(Person subject) {
return new Base(subject);
}
public class Base {
protected Base() {}
private Base(Person subject) { SentenceBuilder.this.subject = subject; }
public Does does(Action action) {
return new Does(action);
}
// also define on() and time() here.
}
public class Does extends Base {
private Does(Action action) {
SentenceBuilder.this.action = action;
}
public void print() {
// …
}
}
}
// example use:
subject(john).does(Action.run).print();
// example misuse:
subject(john).print(); // error: Base has no method named print
[/java]
Type Correlation
“In does(Action.eat).on(x)
, x
must be a Food
item, in does(Action.sing).on(x)
, x
must be a Song
item, etc”.
Level Seven, a simple definition of SentenceBuilder
defines on
as follows:
[java]public SentenceBuilder on(Object object) {
this.object = object;
return this;
}[/java]
Parameter type Object
permits passing Food
or Song
but does not prevent this:
[java]
subject(john).does(Action.eat).on(Song.jingleBells);
[/java]
Level Six, check the type with a switch:
[java]public enum Action {
eat(Food.class), sing(Song.class), …;
public final Class> object;
private Action(Class> object) { this.object = object; }
}[/java]
SentenceBuilder.on()
implementation:
[java]public SentenceBuilder on(Object object) {
Preconditions.checkArgument(object.getClass() == action.object,
Action +” requires an object of type “+ action.object.getSimpleName() +
“, got “+ object.getClass().getSimpleName() +” instead”);
// …
}[/java]
Now doing this:
[java]
subject(john).does(Action.eat).on(Song.jingleBells);
[/java]
will throw IllegalArgumentException: Action eat requires an object of type Food, got Song instead
.
Level One, one method per Action
.
[java]public SentenceBuilder eats(Food f) {
return does(Action.eat, f);
}
public SentenceBuilder sings(Song s) {
return does(Action.sing, s);
}
private SentenceBuilder does(Action a, Object o) {
this.action = a;
this.object = o;
return this;
}
// Example use:
subject(john).eats(Fruit.banana);
// Example misuse:
subject(john).eats(Song.jingleBells); // ← error: Song can’t be cast to Food
[/java]
That option requires duplicating all Action
entries with methods. But does Action
really have to be an enum?
Level One, Typed Constants and Typed Continuations:
[java]public class Action
public static final Action
public static final Action
private Action() {}
// omitted: ways to “switch” actions, get their names, etc
}
public class SentenceBuilder {
// move these two into ‘Does’ if you need to remember the types
Action> action;
Object object;
// …
public class Base {
public
return new Does<>(action);
}
// more common methods
}
public class Does
// constructor…
public Does
// …
}
}
}
// Example use: (does() returns SentenceBuilder.Does
subject(john).does(Action.eat).on(Fruit.banana);
// Example misuse: (“error: Song can’t be cast to Food”)
subject(john).does(Action.eat).on(Song.jingleBells);
[/java]
Return Type Transformers
Suppose, instead of printing a String to standard output, the default behaviour of the SentenceBuilder.print()
method were to return a String. Suppose also that the SentenceBuilder retains the raw data in a “Sentence
” (the Person
instance, the Action
instance, etc, as opposed to immediately piling it all into a StringBuilder
). The Sentence
→String
conversion is done by a Function<Sentence, String>
. Now we want to add a outputWith(Function<Sentence, T>)
method to the builder that lets replacing that default behaviour. Note the “T
” – the client may use a different output type than String
, for instance a Fact
of a third-party logical inference system.
The naive (Level Six) strategy dating from before generics is simply to change the print
return type to Object
:
[java]public class SentenceBuilder {
/** This gets filled by methods subject(), does(), etc */
Sentence sentence;
Function
// …
public SentenceBuilder outputWith(Function
this.output = output;
}
public Object print() {
return output.apply(sentence);
}
}[/java]
Used as:
[java]String sentence = (String)subject(john).does(run).print();
Fact fact = (Fact)subject(john).does(run)
.outputWith(sentenceToFact).print();
[/java]
To have the builder remember the correct output type, we must use a type parameter:
[java]public class SentenceBuilder
/** This gets filled by methods subject(), does(), etc */
Sentence sentence;
Function
/* WARNING – make sure all attributes are set by this constructor. */
private SentenceBuilder(Sentence sentence, Function
// …
}
/** The first method acts as a constructor, starts with default output type. */
public static SentenceBuilder
return new SentenceBuilder<>(new Sentence(subject), sentenceToString);
}
// …
public SentenceBuilder outputWith(Function
return new SentenceBuilder<>(this.sentence, output);
}
public T print() {
return output.apply(sentence);
}
}[/java]
Used as:
[java]String sentence = subject(john).does(run).print();
Fact fact = subject(john).does(run)
.outputWith(sentenceToFact).print();
[/java]
This is Level One at the call-site, but Level Three in the implementation with respect to the state of the builder. If another programmer comes up and adds more state in the object, in addition to sentence
and output
, the outputWith
method will lose information. We could keep the same instance, change ‘output’ in place and return a cast copy with the new type parameter, but other attributes refering to T
will break, client code keeping a reference to the old SentenceBuilder<T>
will break → Level Six, or Seven, depending on how it breaks.
A Level One solution is to encapsulate the output
and the type parameter in an inner class containing only that:
[java]public class SentenceBuilder {
// put all state here
Sentence sentence;
public static SentenceBuilder.Output
SentenceBuilder builder = new SentenceBuilder(subject);
return builder.new Output
}
public class Output
Function
// put all does(), on(), etc, methods here
public outputWith(Function
return new Output<>(output);
}
public T print() {
return output.apply(sentence);
}
}
}[/java]
Balanced Method Calls
“Every from
call must eventually be matched by a to
invocation”
For instance, we want to write
[java]subject(john).from(morning).to(evening).from(monday).to(friday).does(eat).a(banana)[/java]
but also
[java]subject(john).from(monday).from(morning).to(evening).to(friday).does(eat).a(banana)[/java]
where the from-to pairs are nested.
I’ll omit the Level Six implementation, that uses a number to remember how many from
have not yet been closed, check in to
it does not get negative, and check in print
it is zero.
A Level One fluent interface can be understood as a state machine. A state is represented by a Java Type, and a method invocation represents a transition to the type returned by that method. Requirements seen until now could be encoded as finite state machines (plus a kind of object type “register” in the case of type correlation), but now we need to record a number!
The fact that types may take parameters gives the hint on how to achieve that, in a way similar to Church encodings:
- Encode the number zero with a type
Zero
- If the number n is represented by type
T
then number n+1 is represented by typeSucc<T>
(as in successor)
For instance, number three is represented by Succ<Succ<Succ<Zero>>>
. Renaming Zero
and Succ
to Base
and From
for our case, we get something like this:
[java]public class Base {
public From
return new From<>(this, t);
}
public void print() {
// …
}
}
public class From
final T parent;
// code storing ‘t’ somewhere omitted
public From(T parent, Time t) { this.parent = parent; }
public From
return new From<>(this, t);
}
public T to(Time t) {
// omitted: store ‘t’ somewhere
return parent;
}
}
// example use:
base // type Base
.from(morning) // type From
.from(monday) // type From
.to(friday) // type From
.to(evening) // type Base
.print(); // returns void
// missing ‘to’:
base // type Base
.from(morning) // type From
.from(monday) // type From
.to(friday) // type From
.print(); // error: From
// extra ‘to’:
base // type Base
.from(morning) // type From
.from(monday) // type From
.to(friday) // type From
.to(evening) // Type Base
.to(sunset) // error: Base has no method ‘to’
.print();
[/java]
Type-Correlated Balanced Methods
In continuation to the previous example, what if we allowed any Comparable
range, not just Time
, and further required that the to
be of the same type as the input parameter?
In terms of Church encodings this means constructing a list of types. This example shows the state we should encode
[java]from(morning) // [Time]
.from(2) // [Time,Integer]
.from(small) // [Time,Integer,Size]
.to(big) // check we got a Size, then [Time,Integer]
.to(18) // check we got an Integer, then [Time]
.to(evening) // check we got a Time, then []
[/java]
As with numbers, this may be encoded with types. An empty list is represented by Base
, and a list starting with the encoded sequence H and ending with element T is represented From<H,T>
. For instance, [Time,Integer,Size]
is represented as From<From<From<Base,Time>,Integer>,Size>
.
The implementation of the previous example may be modified as follows: > { // do not inherit from Base! // mismatch from-to types:
[java]public class Base {
public
return new From<>(this, t);
}
public void print() {
// …
}
}
public class From
final P parent;
// code storing ‘t’ somewhere omitted
public From(T parent, T t) { this.parent = parent; }
public > From
return new From<>(this, t);
}
public P to(T t) {
// omitted: store ‘t’ somewhere
return parent;
}
}
// example use:
base // type Base
.from(morning) // type From
.from(monday) // type From
.to(friday) // check friday has type Weekday, return From
.to(evening) // check evening has type Daytime, return Base
.print(); // returns void
base // type Base
.from(morning) // type From
.from(monday) // type From
.to(evening) // error: Daytime can’t be cast to type Weekday
.to(friday)
.print();
[/java]