Distinguish Between Optional and Mandatory Parameters in the Builder Pattern
Author
Marcus HeldAfter reading through Designing Bulletproof Code by Otavio Santana I stumbled upon its example of using a builder pattern. While this was not the focus of the article itself I also realized that I saw the issue in the past a lot and I ran in it as well. The widely spread understanding of the builder pattern (as described in Effective Java by Joshua Bloch) does not differentiate between optional and mandatory parameters and that makes their usage not easier but harder.
Let’s start with an example. Imagine you want to use a immutable class called Account
.
class Account {
private @PositiveOrZero long id;
private @NotNull String mail;
private @NotNull String name;
private boolean isAdmin = false;
Account(long id, String name, String mail) {
this.id = id;
this.name = name;
this.mail = mail;
}
Account(long id, String name, String mail, boolean isAdmin) {
this(id, name, mail);
this.isAdmin = isAdmin;
}
}
This class is absolutely user-friendly to use. When you type new Account(<press autocomplete key>
you’ll get a precise understanding which parameters are mandatory and which are not because you can see it by looking at the offered constructors.
While this is nice to handle with such a small amount of parameters it will get very nasty when you have a lot of optional parameters because you’d need to offer all combinations of optional parameters as constructors. So you’d also loose the good readability for the client in the first place.
Another reason to refactor this approach with many parameters is, that the constructors can be very large and, without going into details here, you should avoid any method to have more than (arbitrary number incoming…) 4 parameters.
For the sake of simplicity I’ll continue this example with adding just one more optional parameter called language
.
So a well-known solution to make things simpler again is applying the builder pattern like in the Designing Bulletproof Code article. Doing that for our example would result in this:
class Account {
private @PositiveOrZero long id;
private @NotNull String mail;
private @NotNull String name;
private boolean isAdmin;
private @NotNull String language;
private Account(Builder builder) { // The constructor is private and only called by the Builder
id = builder.id;
mail = builder.mail;
name = builder.name;
isAdmin = builder.isAdmin;
language = builder.language;
}
public static final class Builder {
private long id;
private String mail;
private String name;
private boolean isAdmin = false; // Our Builder sets the default values now
private String language = "en";
public Builder() {
}
public Builder id(long id) {
this.id = id;
return this;
}
public Builder mail(String mail) {
this.mail = mail;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder isAdmin(boolean isAdmin) {
this.isAdmin = isAdmin;
return this;
}
public Builder language(String language) {
this.language = language;
return this;
}
public Account build() {
return new Account(this);
}
}
}
With that code we maybe solved the issue of having too many parameters in the constructor but the usage is not easier now. Using auto-completion of your idea results in this:
So how should you know that
new Account.Builder()
.id(12)
.mail("foo@example.com")
.build();
will produce a validation exception but
new Account.Builder()
.id(12)
.mail("foo@example.com")
.name("Code Hero")
.build();
is valid?
The answer is, you don’t. Except you look into the documentation or into the source code, but that’s tedious, isn’t it? So it would be more robust when the compiler could tell you in the first place how to construct a valid instance of our Account
class. There are two solutions for this problem.
Construct Builder with Mandatory Parameters
One is, only allow the builder to be constructed by setting the mandatory parameters of the final object in the constructor of the Builder
. Doing this for our example builder would look like that:
public static final class Builder {
private long id;
private String mail;
private String name;
private boolean isAdmin = false;
private String language = "en";
public Builder(long id, String mail, String name) { // The mandatory parameters are set here
this.id = id;
this.mail = mail;
this.name = name;
}
public Builder isAdmin(boolean val) {
isAdmin = val;
return this;
}
public Builder language(String val) {
language = val;
return this;
}
public Account build() {
return new Account(this);
}
}
Now there is no way around of setting the correct parameters and understanding that all the others must be optional.
But we can run into the same problem that we wanted to avoid in the first place. With a lot of mandatory parameters our constructor will get large for the builder. I mean this time we’d arguable just have mandatory parameters in their and it might be okay, but a more readable solution would be nice anyway and here we come to the second (but even verboser…) approach.
Use Interfaces to Guide through the Builder
I stumbled upon this approach by reading Builder pattern with a twist by Uzi Landsmann. The idea is having interfaces that guide you through the process of creating a valid instance of the object. For our example it would look like this:
class Account {
private @PositiveOrZero long id;
private @NotNull String mail;
private @NotNull String name;
private boolean isAdmin;
private @NotNull String language;
private Account(Builder builder) {
id = builder.id;
mail = builder.mail;
name = builder.name;
isAdmin = builder.isAdmin;
language = builder.language;
}
// We should offer a static factory method to return the correct interface to start with.
public static Id builder() {
return new Builder();
}
// Our Builder interface methods are in the scope of the Account class.
// For every mandatory parameter we implement a new interface that points to the next in the chain.
interface Id {
Mail id(long id);
}
interface Mail {
Name mail(String mail);
}
interface Name {
Build name(String name);
}
// All optional parameters are in the interface that defines the `build` method.
interface Build {
Build isAdmin(boolean isAdmin);
Build language(String language);
Account build();
}
// Our Builder implements the new interfaces.
private static class Builder implements Build, Id, Mail, Name {
private long id;
private String mail;
private String name;
// We still need to define the defaults of the optional parameters in the builder.
private boolean isAdmin = false;
private String language = "en";
@Override
public Mail id(long id) {
this.id = id;
return this;
}
@Override
public Name mail(String mail) {
this.mail = mail;
return this;
}
@Override
public Build name(String name) {
this.name = name;
return this;
}
@Override
public Build isAdmin(boolean isAdmin) {
this.isAdmin = isAdmin;
return this;
}
@Override
public Build language(String language) {
this.language = language;
return this;
}
public Account build() {
return new Account(this);
}
}
}
Chaining the different Interfaces now results in convenient usage for the client:
But as you can see this is muuuuuuuuch boilerplate we have to write here and I don’t know a library yet that can generate the necessary bytecode for this pattern yet. Maybe this is a nice project to build at some point.
Update
As George Gastaldi pointed out. There is a neat library called Immutables who can generate staged builders for you. I will give it a try asap.
Congratulations for the article. Btw, @ImmutablesOrg helps A LOT in generating builders
— George Gastaldi 🇧🇷 (@gegastaldi) April 29, 2019