Why Builder Is Often an Antipattern and How to Replace it With Fluent Builder
Why Builder Is Often an Antipattern and How to Replace it With Fluent Builder
Safe Alternative To Traditional Builder
Join the DZone community and get the full member experience.
Join For FreeThe Builder Pattern is extremely popular in Java applications. Unfortunately, it's often misunderstood and incorrectly applied, which results to runtime errors.
Let's remember the purpose of Builder: set only necessary fields in some object and keep remaining fields set to default values. For example, if we're preparing a configuration object, then it's convenient to change only the necessary parameters and keep other parameters set to default values.
When Builder Is an Antipattern
Unfortunately, many developers pick only part of the Builder pattern — the ability to set fields individually. The second part — presence of reasonable defaults for remaining fields — is often ignored. As a consequence, it's quite easy to get incomplete (partially initialized) POJO. In an attempt to mitigate this issues we add checks to the build()
method and get a (false) feeling of safety. Unfortunately, by this moment, the main damage is already done: checks are shifted to run time. And to make sure that everything is OK, we need to add dedicated tests to cover all execution paths in code where POJO is created.
How To Fix Builder for POJO's?
First of all, let's define the goal. The goal here is to return checks back to compile time. If code which does not build complete POJOs will not pass compilation, then there will be no need for dedicated tests, no need to perform checks in build()
method. But, most importantly, we will remove a lot of mental overhead from developers.
So, how this can be done? Probably, the most obvious way is to use the Fluent API pattern. The Fluent API has two parts (just like Builder, by the way): provide a convenient way to invoke methods in a chain (both, Fluent API and Builder are identical in this part) and restrict every subsequent call in the chain to only an allowed set of methods.
The second part is what is most interesting for us. By limiting the set of methods that can be invoked at every step of building POJOs, we can enforce a particular sequence of calls and enable the call to the build()
method only when all fields are set. This way, we shift all checks back to compile time. As a convenient side effect, we also make sure that all places where particular a POJO is built look identical. This way, it will be much simpler to spot incorrectly passed parameters or compare changes between code revisions.
To distinguish traditional Builder and Builder with Fluent API, I'll call latter the Fluent Builder.
Deriving Concise Fluent Builder
Let's assume that we want to create a Fluent Builder for simple bean shown below:
xxxxxxxxxx
public class SimpleBean {
private final int index;
private final String name;
public SimpleBean(final int index, final String name) {
this.index = index;
this.name = name;
}
public int index() {
return index;
}
public String name() {
return name;
}
}
Note that I've used Java record getters' name convention in this example. In Java 14, such classes can be declared as records so necessary boilerplate code will be significantly reduced.
Let's add a Builder. The first step is quite traditional:
xxxxxxxxxx
...
public static SimpleBeanBuilder builder() {
return new SimpleBeanBuilder();
}
...
Let's implement a traditional builder first so it will be more clear how Fluent Builder code is derived. A traditional Builder class would look like this:
xxxxxxxxxx
...
private static class SimpleBeanBuilder {
private int index;
private String name;
public SimpleBeanBuilder setIndex(final int index) {
this.index = index;
return this;
}
public SimpleBeanBuilder setName(final String name) {
this.name = name;
return this;
}
public SimpleBean build() {
return new SimpleBean(index, name);
}
}
...
One important observation: every setter returns this and this in turn allows users of this call to invoke every method available in builder. This is the root of the issue, because a user is allowed to call the build()
method prematurely, before all necessary fields are set.
In order to make Fluent Builder, we need to limit the possible choices to only allowed ones, therefore enforcing correct use of the builder. Since we're considering a case when all fields need to be set, then at every building step only one method is available. To do this, we can return dedicated interfaces instead of this
and let Builder implement all these interfaces:
xxxxxxxxxx
...
public static SimpleBeanBuilder0 builder() {
return new SimpleBeanBuilder();
}
...
private static class SimpleBeanBuilder implements SimpleBeanBuilder0,
SimpleBeanBuilder1,
SimpleBeanBuilder2 {
private int index;
private String name;
public SimpleBeanBuilder1 setIndex(final int index) {
this.index = index;
return this;
}
public SimpleBeanBuilder2 setName(final String name) {
this.name = name;
return this;
}
public SimpleBean build() {
return new SimpleBean(index, name);
}
public interface SimpleBeanBuilder0 {
SimpleBeanBuilder1 setIndex(final int index);
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
}
Huh. Somewhat ugly and a lot of additional boilerplate. Can we do better? Let's try.
The first step is to stop implementing interfaces and instead return anonymous classes that implement these interfaces:
xxxxxxxxxx
...
public static SimpleBeanBuilder builder() {
return new SimpleBeanBuilder();
}
...
private static class SimpleBeanBuilder {
public SimpleBeanBuilder1 setIndex(int index) {
return new SimpleBeanBuilder1() {
public SimpleBeanBuilder2 setName(final String name) {
return new SimpleBeanBuilder2() {
public SimpleBean build() {
return new SimpleBean(index, name);
}
};
}
};
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
}
This is much better. We again can safely return SimpleBeanBuilder
from the builder()
method since this class exposes only one method and does not allow users to build instances prematurely. But, much more importantly, we can omit whole setters and mutable fields boilerplate in the builder, which significantly reduces the amount of code. This is possible because we create anonymous classes in the scope where parameters of all setters are visible and accessible. We can use these parameters directly without need to store them!
The resulting code is comparable to the original Builder implementation in regard to total amount of code.
But that's not all. Since all anonymous classes are in fact implementation of the interfaces which contain only one method, we can replace anonymous classes with lambdas:
xxxxxxxxxx
private static class SimpleBeanBuilder {
public SimpleBeanBuilder1 setIndex(int index) {
return name -> () -> new SimpleBean(index, name);
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
}
Notice that the remaining SimpleBeanBuilder
class is very similar to other builder interfaces, so we can replace it with lambda as well:
xxxxxxxxxx
public static SimpleBeanBuilder builder() {
return index -> name -> () -> new SimpleBean(index, name);
}
public interface SimpleBeanBuilder {
SimpleBeanBuilder1 setIndex(int index);
}
public interface SimpleBeanBuilder1 {
SimpleBeanBuilder2 setName(final String name);
}
public interface SimpleBeanBuilder2 {
SimpleBean build();
}
Final touches:
- Move interfaces inside SimpleBeanBuilder interface and do some renaming of interfaces. Since these interfaces extremely rare will appear in user code, we can use some standardized naming for them and simplify automated generation of the code.
- Rename setters, as there is no need to follow Java Bean naming convention because we have no getters here.
- The build() method is not necessary anymore. In original implementation it served as a signal that we're finished assembling POJO, but this is no longer necessary since once last field is set we have all necessary details to build POJO instance.
Below, is the full SimpleBean
code after all these changes applied:
xxxxxxxxxx
public class SimpleBean {
private final int index;
private final String name;
private SimpleBean(final int index, final String name) {
this.index = index;
this.name = name;
}
public int index() {
return index;
}
public String name() {
return name;
}
public static Builder builder() {
return index -> name -> new SimpleBean(index, name);
}
public interface Builder {
Stage1 index(int index);
interface Stage1 {
SimpleBean name(final String name);
}
}
}
Notice that there is very small amount of code which actually does something, most of the implementation is a bunch of interface declarations. Adding, changing or removing fields are quite simple to do, since very little code is involved.
For those who isn't yet used to deeply nested lambdas this code might be harder to get it at first look, but this is matter of experience. Also, there is no need to write such code manually as we can offload this task to IDE (just as we do with traditional builders).
Using described above approach we can replace traditional Builders with Fluent Builders and get Builder convenience with Fluent API pattern safety.
Opinions expressed by DZone contributors are their own.
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}