Fluent interfaces are easy to read. Fluent interfaces are easy to use. Fluent interfaces are natural. That was not a fluent introduction to fluent interfaces. Here's a more fluent introduction: fluent interfaces read like natural language, making them easy to read and use. This article explains what a fluent interface is, how to construct them, and when to use them.
What is a fluent interface?
Fluent interfaces are best explained with examples. Take a look at this JUnit test:
String result = operation();
assertTrue(result.length() > 10);
assertEquals(result.toLowerCase(), result);
assertTrue(result.contains("fish"));
Here's an equivalent test implemented with assertJ's fluent interface:
String result = operation();
assertThat(result)
.hasSizeGreaterThan(10)
.isLowerCase()
.contains("fish");
The assertJ example is easy to read because the syntax reads like a sentence. Someone that has not seen code before can understand it (roughly). The assertJ example is also easy to write because:
- each method has 1 argument: developers do not have to remember the correct order of the arguments
- the editor can auto-complete each method
- minimal code is required to chain another assertion
How do you implement a fluent interface?
Here is the most important point of the entire article: a fluent interface is just a state machine. Once you model the states and transitions, creating a fluent interface is straightforward.
Example: assertJ
Let's implement the fluent interface for the assertJ example. The following methods need to be implemented:
assertThat
hasSizeGreaterThan
isLowerCase
contains
Each method moves from one state to another state, meaning methods are the transitions of the state machine. The first step is drawing the state machine:
This state machine is pretty simple because any of these methods can be called from the same state, so only one state is needed.
The AssertState
state can be modeled as an interface, with each method representing a transition.
Here's the interface for the AssertState
:
interface AssertState {
AssertState hasSizeGreaterThan(int expectedSize);
AssertState isLowerCase();
AssertState startsWith(String prefix);
AssertState contains(String substring);
}
Now we make a class that implements this interface:
class StringAsserter implements AssertState {
private final String actual;
StringAsserter(String actual) {
this.actual = actual;
}
@Override
public AssertState hasSizeGreaterThan(int expectedSize) {
if (actual.length() <= expectedSize) {
throw new AssertionError(String.format("Expecting size of: '%s' to be greater than %s but is %s", actual, expectedSize, actual.length()));
}
return this;
}
@Override
public AssertState isLowerCase() {
if (!actual.toLowerCase().equals(actual)) {
throw new AssertionError(String.format("Expecting: '%s' to be lowercase", actual));
}
return this;
}
@Override
public AssertState startsWith(String prefix) {
if (!actual.startsWith(prefix)) {
throw new AssertionError(String.format("Expecting: '%s' to start with '%s'", actual, prefix));
}
return this;
}
@Override
public AssertState contains(String substring) {
if (!actual.contains(substring)) {
throw new AssertionError(String.format("Expecting: '%s' to contain '%s'", actual, substring));
}
return this;
}
}
That's the hard part! Let's finish this with a nice assertThat
method for the client:
class CustomAssertions {
public static AssertState assertThat(String actual) {
return new StringAsserter(actual);
}
}
And that's it! You made your own fluent interface for string assertions!
Example: SQL Queries
Here is a harder example: a fluent interface for basic SQL queries (inspired by the SQL code generator jOOQ). A SQL query might look like this:
SELECT FirstName, LastName
FROM People
WHERE Age > 25
AND Country = "USA"
ORDER BY LastName
You can express this query in java with a fluent interface like this:
String query = select("FirstName", "LastName")
.from("People")
.where("Age > 25")
.and("Country = \"USA\"");
.orderBy("LastName");
Model your state machine:
Current State | Transition | Output State |
---|---|---|
StartState | select | SelectState |
SelectState | from | FromState |
FromState | where | WhereState |
FromState | build | EndState |
FromState | orderBy | OrderByState |
WhereState | and | WhereState |
WhereState | orderBy | OrderByState |
WhereState | build | EndState |
OrderByState | build | EndState |
Now we create our interfaces from the table values. For each row in the table:
Current State
is the interfaceTransition
is a method on the interfaceOutput State
is the return type of the method
interface StartState {
SelectState select(String column);
}
interface SelectState {
FromState from(String table);
}
interface FromState {
WhereState where(String clause);
OrderByState orderBy(String orderBy);
String build();
}
interface WhereState {
WhereState and(String clause);
OrderByState orderBy(String orderBy);
String build();
}
interface OrderByState {
String build();
}
Let's create a class that implements all of these interfaces:
class QueryBuilder implements StartState, SelectState, FromState, WhereState, OrderByState {
}
Now we add the fields that are needed to hold the query state:
class QueryBuilder implements StartState, SelectState, FromState, WhereState, OrderByState {
private List<String> columns;
private String table;
private List<String> conditions = new ArrayList<>();
private String orderByColumn;
}
Then we implement the interface methods.
Notice how the implementation is very similar to the builder pattern.
The main difference is each method returns a different type (the next state) instead of the same QueryBuilder
.
class QueryBuilder implements StartState, SelectState, FromState, WhereState, OrderByState {
private List<String> columns;
private String table;
private List<String> conditions = new ArrayList<>();
private String orderByColumn;
@Override
public SelectState select(String column) {
columns = List.of(column);
return this;
}
@Override
public FromState from(String table) {
this.table = table;
return this;
}
@Override
public WhereState where(String clause) {
this.conditions.add(clause);
return this;
}
@Override
public WhereState and(String clause) {
this.conditions.add(clause);
return this;
}
@Override
public OrderByState orderBy(String orderBy) {
this.orderByColumn = orderBy;
return this;
}
@Override
public String build() {
String selectComponent = "SELECT " + String.join("," , this.columns)
+ " FROM " + table;
String whereComponent = conditions.isEmpty()
? ""
: " WHERE " + String.join(" AND ", conditions);
String orderByComponent = orderByColumn == null
? ""
: " ORDER BY " + orderByColumn;
return selectComponent + whereComponent + orderByComponent;
}
}
Fluent Interface cookbook
Here are the steps for creating a fluent interface:
- Model your fluent interface as a state machine.
- Convert your state machine to a set of interfaces.
- Each state is an interface.
- Each transition is a method, where the current state is the interface the transition belongs to and the next state is the return type of the method.
- Create a class with all the fields you need.
- Implement all your interfaces in your class. Each method mutates something in your class and then returns
this
.- Each method can return a different interface, but
this
can be returned because it implements all of the interfaces.
- Each method can return a different interface, but
When should you use fluent interfaces?
Hopefully you are convinced that fluent interfaces are a powerful concept that promote easy to read and write code; however, the actual implementation of the fluent interface is not very simple. You should consider if the complexity of implementing the fluent style is worth it (in many cases it is worth it).