Fluent Interfaces

Apr 1, 2023

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
Note
Method chaining and fluent interfaces are not the same thing. Method chaining is just one of the techniques used to create fluent interfaces.

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:

PlantUML diagram (light)

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);
    }
}
Note
The return type of the method is the interface AssertState NOT the implementation StringAsserter. You do not want to expose your implementation to the client.

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");
Note
JOOQ defines constants for table and column names. I'm not using them in this article because it adds unwarranted complexity to the example, but it is good practice to use them.

Model your state machine:

PlantUML diagram (light)
Current StateTransitionOutput State
StartStateselectSelectState
SelectStatefromFromState
FromStatewhereWhereState
FromStatebuildEndState
FromStateorderByOrderByState
WhereStateandWhereState
WhereStateorderByOrderByState
WhereStatebuildEndState
OrderByStatebuildEndState

Now we create our interfaces from the table values. For each row in the table:

  • Current State is the interface
  • Transition is a method on the interface
  • Output 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:

  1. Model your fluent interface as a state machine.
  2. Convert your state machine to a set of interfaces.
    1. Each state is an interface.
    2. 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.
  3. Create a class with all the fields you need.
  4. Implement all your interfaces in your class. Each method mutates something in your class and then returns this.
    1. Each method can return a different interface, but this can be returned because it implements all of the interfaces.

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).