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
:
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:
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:
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
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:
The Developer
type is now wholly immutable, and can only be created by using the builder.
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:
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:
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:
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:
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)