How Fluent Interfaces Can Lead to More Meaningful Code

Martin Fowler, who coined the term Fluent Interfaces 15 years ago, famously wrote in his book Refactoring: Improving the Design of Existing Code:
“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
Table of Contents
Software development is about communication.
The eventual recipient might be a compiler or interpreter, running our code as a series of ones and zeros. Computers don’t care about the readability of source code, but the other participants in the communication do: humans.
It’s about one developer communicating with another one. Or with ourselves in the future, when our code appears to us like it was written by someone else.
I’ve written about the importance of the visual representation of code before, this article will highlight the concept of fluent interfaces and how we can incorporate it into our codebases for instance creation. But it will also show the downsides of using this particular API style.
All code examples will be in Java, or reference Java frameworks. But the general principles of fluent interfaces apply to any other language, too.
Fluent Interfaces
As with many other design patterns, the core concept can be summarized in a short sentence:
A fluent interface is designed to mimic a Domain-Specific-Language (DSL) by relying heavily on method-chaining.
In object-oriented programming, method-chaining is a way of invoking multiple method-calls successively. This allows us to write our code in a fluent, more straightforward way, and reduce visual noise.
To achieve the ability to actually use method-chaining, methods can’t return void
or another arbitrary value.
They must return the most logical value for the given context.
There are three ways in which we can have more fluent code.
The mutable way
Each setter or related method of an instance mutates the instance itself and returns this
.
This way of fluency isn’t appropriate for instance creation, at least in my opinion. We gain method-chaining, but that’s all. It should be reserved for non-POJO types, e.g., fluent interfaces for workflows.
The immutable way
Instead of returning the instance itself, a new instance representing the mutated state is returned.
A prime example of this behavior is the java.time
API, or java.math.BigDecimal
:
// CORRECT
BigDecimal basePrice = new BigDecimal("9.99");
BigDecimal withTaxes = basePrice.multiply(new BigDecimal("1.19"));
// INVALID
BigDecimal basePrice = new BigDecimal("9.99");
basePrice.multiply(new BigDecimal("1.19")); // won't modify basePrice
With intermediary builder
The type itself doesn’t have to be fluent. We could use an additional fluent auxiliary type for instance creation, e.g., a fluent builder.
In Java, we can use builders to get around the lack of named parameters.
How to Create a Fluent Builder
We start with a type describing a developer:
public class Developer {
private String email;
private String name;
private List<String> languages;
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getLanguages() {
return this.languages;
}
public void setLanguages(List<String> languages) {
this.languages = languages;
}
}
A simple POJO, just storing some data, without any extra functionality.
Creating a simple builder
To improve instance creation, we need an additional builder type:
public class DeveloperBuilder {
private String email;
private String name;
private final List<String> languages = new ArrayList<>();
public DeveloperBuilder email(String email) {
if (email == null || email.length() == 0) {
throw new IllegalArgumentException("email musn't be null/empty");
}
this.email = email;
return this;
}
public DeveloperBuilder name(String name) {
this.name = name;
return this;
}
public DeveloperBuilder language(String lang) {
if (lang == null || lang.length() == 0) {
throw new IllegalArgumentException("lang musn't be null/empty");
}
if (this.languages.contains(lang)) {
return this;
}
this.languages.add(lang);
return this;
}
public Developer build() {
if (email == null || email.length() == 0) {
throw new IllegalStateException("email is a required field");
}
if (this.language.isEmpty()) {
throw new IllegalStateException("at least one language must be added");
}
Developer developer = new Developer();
developer.setEmail(this.email);
developer.setName(this.name);
developer.setLanguages(this.languages);
return developer;
}
}
By using the builder pattern, we still have our simple POJO type. But now instance creation can also have validation during preparing the builder and at creating the actual instance.
Using the builder
Developer validDev =
new DeveloperBuilder().email("ben@example.com")
.name("Ben Weidig")
.language("Java")
.language("Swift")
.language("Golang")
.build();
Developer invalidDev1 =
new DeveloperBuilder().name("Ben Weidig")
.language("Java")
.language("Swift")
.language("Golang")
// throws IllegalStateException
.build();
Developer invalidDev2 =
new DeveloperBuilder().email("ben@example.com")
.name("Ben Weidig")
// throws IllegalArgumentException
.language(null)
.language("Swift")
.language("Golang")
.build();
The fluent builder separates the code for construction and representation.
We gained a more understandable way of creating instances, with additional control, like validation, during the construction process. Especially complex types can be improved with builders.
But we can improve our builder, and also the corresponding type, even more.
Improving the builder with immutability
An often-used design is to make the builder a nested class of its corresponding type. This also means giving up a pure POJO, but gaining an immutable type instead, which is a great bargain in my opinion:
public class Developer {
private final String email;
private final String name;
private final List<String> languages;
// private constructor, so we are forced to use the builder
private Developer(String email,
String name,
List<String> languages) {
this.email = email;
this.name = name;
// make languages collection immutable, too
this.languages = Collections.unmodifiableList(languages);
}
public String getEmail() {
return this.email;
}
public String getName() {
return this.name;
}
public List<String> getLanguages() {
return this.languages;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String email;
private String name;
private final List<String> languages = new ArrayList<>();
// builder needs to be created with static method on actual type
private Builder() { }
public Builder email(String email) {
if (email == null || email.length() == 0) {
throw new IllegalArgumentException("email musn't be null/empty");
}
this.email = email;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder language(String lang) {
if (lang == null || lang.length() == 0) {
throw new IllegalArgumentException("lang musn't be null/empty");
}
if (this.languages.contains(lang)) {
return this;
}
this.languages.add(lang);
return this;
}
public Developer build() {
if (this.email == null || this.email.length() == 0) {
throw new IllegalStateException("email is a required field");
}
if (this.languages.isEmpty()) {
throw new IllegalStateException("at least one language must be added");
}
return new Developer(this.email,
this.name,
this.languages);
}
}
}
The Developer
type is now wholly immutable, and can only be created by using the builder.
Developer valid = Developer.builder()
.email("ben@example.com")
.name("Ben Weidig")
.language("Java")
.language("Swift")
.language("Golang")
.build();
Immutable builder generation
Let’s be honest… that’s still a lot of additional code. And we would need to write it for every POJO-like type.
Instead of doing it ourselves, we can use frameworks like Immutables:
@Value.Immutable
public interface Developer {
String getEmail();
@Nullable
String getName();
@Value.Default
default List<String> getLanguages() {
return Collections.emptyList();
}
}
Thanks to annotation processing, our builders are generated for use, including validation, more builder methods, easy copying, and a lot more features than we could ever do ourselves:
Developer dev = ImmutableDeveloper.builder()
.email("ben@example.com")
.name("Ben Weidig")
.addLanguages("Java")
.addLanguages("Swift", "Golang")
.build();
Fluent Interfaces for Workflows
Even though this article is mainly about fluent builders, it’s not the only way to use fluent interfaces. They are widely adopted for any kind of multi-step workflow.
An excellent example of fluent API design is the Java Streams API. We can reduce a lot of verbosity by method-chaining Stream-related calls together, instead of using the traditional means of iterating over a collection:
// "TRADITIONAL" WAY OF ITERATING
List<Book> books = ...;
List<String> result = new ArrayList<>();
for (Book book : Books) {
if (book.getPublishYear() != 2020) {
continue;
}
if (book.getGenre() != Genre.SCIENCE_FICTION) {
continue;
}
Author author = book.getAuthor();
String sortableName =
String.format("%s, %s",
author.getLastName(),
author.getFirstName());
result.add(sortableName);
if (result.size() == 5) {
break
}
}
Collections.sort(result);
A lot of code for getting the names of the first five science fiction authors of 2020 in our collection.
Let’s make this call fluent with streams:
// FLUENT WITH STREAM
List<Book> books = ...;
List<String> result =
books.stream()
.filter(book -> book.getPublishYear() == 2020)
.filter(book -> book.getGenre() == Genre.SCIENCE_FICTION)
.limit(5)
.map(Book::getAuthor)
.map(author -> String.format("%s, %s",
author.getLastName(),
author.getFirstName()))
.sorted()
.collect(Collectors.toList());
If we include the formatting and brackets, we managed to reduce 20 lines of code down to ~10 lines.
The reduced clutter is great. But in my opinion, improved comprehensibility is way more important. The concise names of the Stream operations convey their intent clearly.
Conclusion
Fluent interfaces are great for consumption, but not for creation.
Creating a more DSL-like API helps developers to consume it with less verbose, more comprehensible code.
We get more control over how we create an object, even splitting it into multiple steps. Validation can be integrated at every step. And it helps to separate the code for creation from the actual representation.
But creating an enjoyable, fluent API isn’t an easy task. We need additional auxiliary types, which means a lot of extra, and often repeated, code.
Another downside is creating the possibility for more runtime-errors. If our type needs all its required parameters in the constructor, we would directly see what’s required. With a builder, we might miss them and won’t realize until runtime.
Debugging can also be harder if our IDE doesn’t support breakpoints at different points in the chain. This can be often overcome by breaking the chain into multiple lines, which coincidentally improves readability, too.
At my company, we started using a fluent builder last year, thanks to the Immutables framework. We started by using it only for new types, but also replaced older types over time, if appropriate.
Sometimes it wasn’t easy to integrate immutable types into the existing code. But it gave us a chance to reevaluate our API design and improve on it.
Resources
- FluentInterface (Martin Fowler)
- Fluent Interface (Wikipedia)
- Immutables framework
- Implementing the builder pattern in Java 8 (Vogella)