The right time to build
We’ve talked at length about the costs of builders, and yet… they persist. I can think of at least three examples from popular java libraries that work well:
- Hamcrest’s Description
- Java’s own StringBuilder
- Guava’s ImmutableList.Builder
Although, having looked at StringBuilder
‘s doc – augh! It’s so big!
Five or six builders could come out of that, starting with
AppendOnlyStringBuilder
.
What do these master builders (That’s enough linguistic hyphens between
the Builder
pattern and actual builders – Ed) have in common?
Well, they all have trivial build implementations. Really though,
that’s an artifact of the thing that they’re building – there’s a well
defined null type for each – Description
‘s is simply an empty
description, StringBuilder
‘s is ""
, and ImmutableList.Builder
‘s is
simply ImmutableList.of()
. All three of these things are essentially
list builders: Description
and StringBuilder
are looking after
[Char]
, and ImmutableList.Builder
is looking after some appropriate
[T]
.
This gels well with good experiences I have had with builders in code
I’ve been near: applications with publishers and subscribers, tuples
with arbitrary numbers of arbitrarily typed fields, user lists built
from clicks in a UI, before a batch action is selected. When you hit
build
on all of these things, the type you get back is either [T]
,
or a type that depends heavily on a [T]
Careful now
We could quite easily conclude here that the only good builders are those that build lists. The original point bears repeating:
Builders work best when there’s a well defined null type for the thing being built.
Even that, though, is not quite right. It’s quite possible for a
Builder
to work nicely if the thing being built is a composite of
several types, each with a well defined null type. The following
definition might be better:
Builders work best when it’s impossible for them to be in a state where invoking
build
generates an invalid object
That feels like a corollary of this, though:
Builders work best when all of their fields have a well defined null type
In addition, the interplay between the dependencies should also have a well defined null type. Defining that, however, is tricky, so I will assume we’re all one the same page and gracefully move on to defining it for single fields…
A well defined null type
Some examples:
List
‘s null type is the empty listString
‘s null type, is, unsurprisingly, an emptyString
, given that aString
is just aList
ofchar
s.
Arbitrary objects do not have a well defined null type. null
in
Java is not well defined, because at some point it’s either going to
cause NPE, or a giant tangle of obj == null ? doStuff(obj) : panic();
cruft. Using null
to signify emptiness or default values in builders
is a crime I hope no-one is tempted to commit.
So, each field can either be a primitive, for which a default value can
be specified, an interface, for which a default implementation is
specified (perhaps a no-op one), or a type like List
, Iterable
or
Maybe
(these have well defined null types). We could start talking
about builders within builders here, but we won’t.
Cowboy Builders (You’re fired – Ed)
It’s difficult not just to define these as being !good builders. So instead, here are some things we may see if a builder is being used inappropriately (I say!):
Unnecessary builders
Fewer than four fields. One usage. Inline with extreme prejudice.
Irresponsible builders
Built objects propagate across the system after construction, and each part of the system does some more validation of the object to see if it is suitable.
Perhaps each receiver needs a slightly different builder? Perhaps they
all need the same builder, and we’ve found a case where a non trivial
build
implementation is required, which performs the validation just
once.
Alternatively, this could be because of null
or sentinel defaults
(Long.MAX_VALUE
, anyone?) leaking out from the built object. We can
either establish a sort of null protocol at call sites or find a more
appropriate default.
A simple example null protocol for Maybe
unfriendly lingoes: null
can never escape. hasField
is obvious and is elided.
void doThingWithFoo() {
if (hasField()) doThing(getField()) else doThing(default());
}
Foo getField() {
Preconditions.checkState(hasField(), "go away!");
return fooField;
}
Overexposed builders
Often, the fluent style of building is used to mimic the sort of pass-by-name arguments we see in more fun programming languages:
my_burrito = burrito(size: "large", filling: "pork",
beans: "both", salsa: "hot",
guacamole: "yes please")
final Burrito myBurrito = newBurritoBuilder().withSize(Size.Large)
.withFilling(Filling.Pork)
.withBeans(Beans.Both)
.withSalsa(Salsa.Hot)
.withGuacamole()
.build();
So. This is the first quandary I find myself in. I really like this style, it’s descriptive, leaving me confident that the constructed burrito will be as requested. How many of these fields really have a well defined null type, though? What are the mandatory requirements for a burrito? Could we just use a builder for the optional parts? Perhaps the following might be more appropriate:
Burrito myBurrito =
new Burrito(Size.Large, Filling.Pork, Beans.Both,
options().withSalsa(Salsa.Hot).withGuacamole().build());
We should carefully match the fields we use a builder for based on whether the object about to be constructed really needs them or not. Exposing the mandatory dependencies as builder parameters is potentially confusing and costly.
As my friendly, local ruby guru points out though, you can get the nice fluency of this pattern without the cost:
@grumpyjames Internal builders (encapsulating defaults) exposed through public factory methods for the common cases ftw.
— Garry Shutler (@gshutler) August 10, 2013
Broadly I agree with this. I have seen times where it works beautifully, particularly in creating objects in test. There have also been times, however, where I have come across that pattern, felt it inappropriate and simply inlined until the builder disappeared.
Conclusion
We now have a reasonable guideline for when to use builders, and some example smells which might allow us to spot a cowboy more quickly (I give up – Ed).
Where now?
If this series manages the difficult third album, we’ll perhaps start to touch on more general object creation concerns, and perhaps a little bit of wiring.