
Cascading Lambdas and Builder Pattern in Java, when 1 + 1 = 3… but not 4
The builder pattern is one of many well known design patterns used to manage complexity in our code. In particular, it helps us to separate an object construction from its representation, which is especially useful when we have to construct an object with a large number of fields. But what it doesn’t do well is to discriminate between mandatory and optional fields at construction time. This post will explore different ways to overcome this limitation and introduce a Java 8 specific feature to hopefully make the builder pattern implementation more robust while keeping the code simple and concise.
Let’s first start by assuming we want to create an object of type Person
, which could look something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public final class Person { private final String firstName; private final String lastName; private final LocalDate dateOfBirth; private Person(String firstName, String lastName, LocalDate dateOfBirth) { this.firstName = requireNonNull(firstName, "firstName cannot be null"); this.lastName = requireNonNull(lastName, "lastName cannot be null"); this.dateOfBirth = dateOfBirth; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public Optional<LocalDate> getDateOfBirth() { return Optional.ofNullable(dateOfBirth); } } |
For brevity we only declare 3 fields here, but we could easily have 10-12 fields (although too many declared fields might also be a code smell), which is one of the problems the builder pattern solves by avoiding the explosion of constructor parameter combinations (assuming not all fields are mandatory). Above is a typical immutable data class (all fields are final
, no setter method), the constructor is also private as we want the class to only be instantiated by the builder. In the example above, we notice from the getter method signatures that we want dateOfBirth
to be optional and firstName
and lastName
to be mandatory.
So how can we force the creation of Person
with firstName
and lastName
while leaving the possibility of not initializing the dateOfBirth
? We could use a typical variation of the builder pattern, called the Step Builder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public interface FirstNameBuilder { LastNameBuilder firstName(String firstName); } public interface LastNameBuilder { PersonBuilder lastName(String lastName); } public static FirstNameBuilder builder() { return new PersonBuilder(); } public static class PersonBuilder implements FirstNameBuilder, LastNameBuilder { private String firstName; private String lastName; private LocalDate dateOfBirth; private PersonBuilder() { } @Override public LastNameBuilder firstName(String firstName) { this.firstName = firstName; return this; } @Override public PersonBuilder lastName(String lastName) { this.lastName = lastName; return this; } public PersonBuilder dateOfBirth(LocalDate birthday) { this.dateOfBirth = birthday; return this; } public Person build() { return new Person(firstName, lastName, dateOfBirth); } } |
By making PersonBuilder
implement FirstNameBuilder
and LastNameBuilder
interfaces, also by making its constructor private and by exposing only FirstNameBuilder
as an entry point through the builder()
static method, we indeed force the user to initialize firstName and lastName before making the build()
method available. The following is an example of how the builder would be invoked:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Person person = Person.builder() .firstName("John") .build(); // Doesn't compile Person person = Person.builder() .firstName("John") .lastName("Doe") .build(); Person person = Person.builder() .firstName("John") .lastName("Doe") .dateOfBirth(LocalDate.of(1998, DECEMBER, 19)) .build(); |
So we are done! Or are we? On the surface everything looks good now, the user of our builder cannot forget to call neither firstName()
nor lastName()
, those rules now being enforced by the compiler. Here we are assuming the user of our builder acts as a good citizen with our api and will obey the to rules we just set. But what if the user of our builder decides to go rogue and tries to trick us? In the spirit of never underestimating the creativity and determination of people wanting to do bad things (in coding or in life in general as a matter of fact), is there a way to bypass the safeguards we just implemented above and instantiate a Person
object through the builder without first calling firstName()
and lastName()
?
1 |
Person person = ((PersonBuilder)Person.builder()).build(); |
Looks like the answer is “yes”. A simple cast to PersonBuilder
is enough to bypass all the rules we tried to enforce above. So how can we prevent this? We need to return an implementation of FirstNameBuilder
from Person.builder()
that doesn’t declare the build()
method. We also need to do the same with LastNameBuilder
. But if we have to declare a new class for each mandatory parameter exposed by our builder, our code will start to get pretty bloated and hard to follow. So how can be achieve this in a more concise and elegant way?
Introducing Cascading Lambdas
I invite you to read this excellent and very detailed article from Venkat Subramaniam which explains in a very accessible way what are Cascading Lambas, but for the scope of this blog post, it could be very quickly summarized as the ability to have functions that can return other functions. If you notice from above, FirstNameBuilder
and LastNameBuilder
are interfaces declaring a single abstract method, thus they are considered functional interfaces since Java 8. We can also see that the initial Person.builder()
call returns a functional interface (FirstNameBuilder
) which in turn returns another functional interface (LastNameBuilder
). By mapping those functional interfaces to lambda expressions, we indeed get a function that returns another function.
So how this can be combined with the builder pattern we created above :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
@FunctionalInterface public interface FirstNameBuilder { LastNameBuilder firstName(String firstName); } @FunctionalInterface public interface LastNameBuilder { PersonBuilder lastName(String lastName); } public static FirstNameBuilder builder() { return firstName -> lastName -> new PersonBuilder(firstName, lastName); } public static class PersonBuilder { private final String firstName; private final String lastName; private LocalDate dateOfBirth; private PersonBuilder(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public PersonBuilder dateOfBirth(LocalDate birthday) { this.dateOfBirth = birthday; return this; } public Person build() { return new Person(firstName, lastName, dateOfBirth); } } |
Our builder()
method highlighted above uses cascading lambdas to chain the calls to set our mandatory parameters firstName()
and lastName()
. This can be read as “create a function that takes parameter firstName and return another function, that new function taking parameter lastName and returns a PersonBuilder
“. Only when we have an instance of PersonBuilder
that the build()
method is made available to instantiate a Person
object. See below how our interfaces are being made available as we chain the method calls when we instantiate a Person
:
We make the final build()
method available only once all our mandatory parameters are set, we also prevent cheating from prematurely get access to the build()
method through explicit casting:
1 |
Person person = ((PersonBuilder)Person.builder()).build(); // will throw ClassCastException |
So there you have it, by combining the builder pattern with cascading lambdas (1 + 1), we get much safer, concise and readable code (3), which hopefully is of bigger value than the sum of each part… Speaking of this blog post’s title, why 1 + 1 = 3… but not 4? The code above lets the compiler itself prevent the coder from making mistakes that would normally show up at runtime, it also cuts the number of runtime checks that has to be implemented, but not all of them. We still need to check at runtime for null values explicitly passed as our mandatory parameters firstName()
and lastName()
since the following is still possible:
1 2 3 4 |
Person person = Person.builder() .firstName(null) .lastName(null) .build(); |
It would be nice to also be able to catch this scenario at compile time… which is possible… in Kotlin!
9 Replies to “Cascading Lambdas and Builder Pattern in Java, when 1 + 1 = 3… but not 4”
You don’t really need cascading lambdas, or a curried builder, like that. It can get quite unwieldy for more fields. Just returning method references from builder methods would be enough. And it would also allow for fine-grained invariants over multiple sets of fields…
Thanks Charlie for taking time to read my post, constructive comments are always appreciated, if you have time and can provide a small code snippet to illustrate your argument that would be very appreciated, I want to make sure I correctly understand and don’t misinterpret the argument you are trying to make before I try to reply to the specifics of your comment. I’ll say this in general though (which might not apply to your comment), my philosophy is to try to keep it as simple as possible for the user of the api, sometimes at the expense of adding a bit of complexity in the implementation. For example, one @FunctionalInterface per mandatory parameter (not optional parameters) could make the implementation of the builder a bit bloated as more and more mandatory parameters are needed, but this makes it easier and more natural for the caller of the builder e.g.
Chaining (or cascading) those @FunctionalInterfaces also make it possible to never make the build() method of the builder visible until all mandatory parameters are set.
Well, my argument was not against chaining restricted views of the builder via functional interfaces. Just that the same can be achieved via a maybe-more-extensible way with method references:
class Person {
static FirstNameBuilder builder() {
return new PersonBuilder()::firstName;
}
static class PersonBuilder {
LastNameBuilder firstName(String firstName) {
this.firstName = firstName;
return this::lastName;
}
GenderBuilder lastName(String lastName) {
this.lastName = lastName;
return this::gender;
}
PersonBuilder gender(Gender gender) {
this.gender = gender;
return this;
}
// ...
}
}
I’m glad you provided a small code example as I clearly misunderstood the point you were trying to make in your initial comment. This is a very interesting approach indeed, the only thing I can see is the fact that we could then call multiple times the same methods for mandatory fields. If we use the following as an example:
the only minor difference is that we could then potentially do something like:
which would not compile with the cascading lambdas (although we can’t avoid this scenario anyway with optional fields e.g.
dateOfBirth
), but otherwise I really like your idea of returning method references in general when we can, it’s not only useful as input parameters to methods and can make the code much cleaner!You can set scope of all setters to private and example with method references will be working as lambda chain example 🙂
You are right, by changing the access level of the mandatory fields setters to private (
firstName()
andlastName()
here) onPersonBuilder
, we can achieve the same result as going the cascading lambdas route :). At this point only accessing the setters on thePersonBuilder
through reflection (withsetAccessible(true)
) would bypass the mechanism, which would not be possible with cascading lambas asfirstName()
andlastName()
would not even exist, but here we are crossing the line of what is reasonable ;). Thanks to you and Charlie for your contributions, that is also why we write blog posts, to generate discussions that can lead to alternative ways of doing things, this was a good one!Interesting post. Can you provide the final code? Thank you.
Thanks for the nice comment! You can take a look at the complete code here https://gist.github.com/pellse/6cefa4503a9b9015f71ca7dc8cf13fe8. If you would like to keep the Person class as a “pure” data class and have the builder in a separate class, this would be another example https://gist.github.com/pellse/a8454c8134f56cb981375d87307b4d1b.
Thanks, it’s really nice and useful article, to check for nulls at runtime you can try this:
public static FirstNameBuilder builder() {
return firstName -> lastName -> new PersonBuilder(Objects.requireNonNull(firstName), Objects.requireNonNull(lastName));
}