Abuse of the Builder pattern
The Builder pattern is a well-known construct in the Java world, and doesn’t show any sign of diminishing in popularity. It’s used in countless popular libraries, and most IDEs have an automated tool for creating a Builder for your type. It’s a convenient way of handling optional parameters in the creation of an object without having to write a cartesian product of alternative constructors, factory methods, or sacrificing immutability of the created object. It has deviated a little over time from original “Gang of Four” definitions, so in case you’re not familiar with the modern case, the usual example looks a little like this:
As you can see, it is somewhat verbose on the implementation side, but when the number of optional or defaulted parameters gets high, then it starts to look rather sensible at the usage site:
This makes them ideal for things like configurations, where there is usually a default configuration and what you want to specify any deviations from it without specifying the whole thing again.
So what’s the problem?
Builders are great for configuring things, but recently I’ve seen them used a whole lot more in the context of data records. If you take a look around the data object representation and serialization space, there’s a whole lot of Builders being generated as part of the implementation code. Avro generates Builders to create its record classes, Protobuf won’t let you create records using any other method than a builder, and the default implementation in Immutables is to create the immutable types with a builder.
In every one of these cases, using the Builder is required to specify the *required fields as well as the optional or defaulted ones.*
If required fields are specified using a Builder, you are transferring the burden of knowing which fields are required from the compiler on to the developer. This is very bad, because we all know that relatively speaking compilers are smart and developers are stupid. If you forget to specify a field it becomes a runtime error instead of a compile-time error, and we lose some precious safety that we had for free in the land of the constructor.
In the Person example above, the following code would compile, but result in a Person with a null name, which is probably not what you want:
What’s the alternative?
1: Split your Builder into parts
Using this method you put your required fields into the constructor or factory method of the Builder, then have all optional mutations as methods on the Builder. This makes it impossible to create an invalid object, while still giving you the flexibility to specify a subset of the optional functionality. That makes the usage look a bit more like this:
It is now impossible to create a Person without a name.
2: Use Optional<> fields
The concept of nullable fields has been around for a while, but null is problematic for many reasons. Luckily, Java 8 introduced a standardized Optional type which is a safer alternative. Using this you make it explicit by the type definition which fields are Optional, making it safe to implement Person like this:
It’s much uglier in usage as the Builder variant, but the implementation is much simpler and shorter and removes the need for a whole extra class to be created.
3: Immutable alterations
If the number of optional fields is low and you’re not allocating huge numbers of these objects, then you could start with an minimum object specified using just the required fields and make immutable edits to it to return variations with the content you need.
I think this is my favourite of the alternatives, but it comes at a cost. Each mutation will create a new object, which will have to be cleaned up later. This is fine for application objects, but if you’re doing data processing this will soon become a bottleneck and then you should probably consider option 1 instead.
4: Factory methods
If you’re trying to replicate just a few different concrete ways to construct an object, you might consider using factory methods instead:
The Builder pattern is useful in a range of situations, but you shouldn’t think of it as a naive drop-in replacement for the named parameter feature that Java lacks. By doing so with required fields you move the burden of checking correctness from the compiler to the developer, and open the door to possible runtime errors. There are many good alternatives for this use case so you should consider them before reaching for the all-args Builder every time, and library authors who deal with data records should take the same advice.
I’ve had some correspondence on the subject that I think is worthy of note here. @whitingjp pointed out this rather sensible fifth alternative:
5: Accessible default values
A simple solution to this is to provide a single all-args constructor and have default values publicly and statically available to insert into it. This works similarly to (2), but with less developer and execution overhead. It relies on the convention of people knowing where to find the default values, but if you’re happy to make it a standard for your project or team it could be a good option for fields with default values.
Finally, Mārtiņš pointed out one benefit of the Builder that I overlooked, and that’s that it protects you against reordering of constructor args of the same type; meaning that if you had a
new Person(firstName, lastName) and switched the signature to
new Person(lastName, firstName), then you wouldn’t even get a runtime error, just bad data. I don’t think that kind of sneaky behaviour should ever be encouraged, so in balance I still don’t think that’s worth having a Builder for. I did, however, knock up this solution which is very silly but fixes that problem too. I do not recommend that you do this in real life.