Some months ago, I read the post referenced in this tweet:
How to solve the Builder pattern’s boilerplate problem in #Javahttps://t.co/EfqeAwqJhZ pic.twitter.com/d1kVBTZQ30
— Java (@java) May 19, 2018
In short, the article provides two ways to generate the boilerplate code required by the Builder pattern:
- Lombok, to generate code at compile time
- The Spark Eclipse plugin, to generate code at development time
However, I already wrote about the Builder pattern. And it’s a bit more complex than what’s described in the referenced post.
I gave it some thoughts, and I came up with 3 different levels of complexity in the Builder pattern. Only the first one can be managed by the above solutions.
Basic Builder
Let’s start with a simple example, a Person builder. The Person can have three arguments: a title, a first name and a last name. It’s a very contrived example, and doesn’t require a Builder, but it serves its purpose. Bear with me for the sake of explanation.
Let’s model this Builder as a finite state machine.
There’s a single state, and withXXX()
methods are transitions going from and to it:
At this point, the Builder’s only reason for being is improve on the situation when a constructor has many arguments. IMHO, compared to other cases below, it’s a degenerate Builder, as the finite state machine only has one state.
Required and optional arguments
Now, let’s add an additional constraint: compared to the previous case, the last name argument is now required.
A possible option would be to add the argument to the builder constructor. While it’s possible for a small number of arguments, it soon becomes unmanageable if this number increases. Another alternative is use the finite state machine (again):
- The first state is designed around a builder without a
build()
method: a builder that is invalid - The second state is designed around a builder with a
build()
method
And the only way to go from the first state to the second is by passing the first name argument.
Hierarchical builders
The previous paragraph was about required and optional arguments. Let’s add one more level of complexity. Imagine a pizza builder, allowing to choose two different pizza layers: the sauce base, and the different ingredients.
However, some ingredients should only be available for specific bases. For example, it’s only possible to add pineapple on tomato base, while salmon can only be added to sour cream base.
I profusely apologize for my Italian readers who believe that putting pineapple on pizzas is a crime against Italian cuisine. This is only meant to illustrate my explanation. |
The corresponding finite state machine can be modeled as such:
Of course, additional requirements can then be added as states and transitions to the above. |
Conclusion
As seen from the above, builders can handle complex use-cases. The simplest one - described in the first paragraph is an easy target for code generation, either at bytecode or at source level. Other cases described in the other two paragraphs are not so easily handled.
To generate code in a sufficiently generic way to handled every approach, I believe the best way is to model the Builder as a finite state machine.