RE: Design Patterns: Redesign patterns. You’ll eventually need to do it or you’ll stumble over false rigidity. Here’s an experience I had teaching a colleague a bit more about design flexibility and the builder design pattern.
Index
Prerequisite Knowledge
Design patterns in software are well-established patterns that solve a given problem well. In an effort to Keep the Internet DRY, for any further background you need on the broad topic of design patterns, I will rely on and direct you to one of my favourite free design pattern references, Refactoring Guru. Of the many important concepts surrounding design patterns, I want to hone in on just a couple:
- Each pattern is a customizable blueprint
- Design Patterns provide a common language within software teams
Design patterns are a deep mine of valuable resource, and these are the core concepts to point delvers in the right direction.
Lateral Examples
This section is fun and valuable, but a bit of a digression. If you want to stick to the essentials, skip ahead to Colleagues & Code Reviews.
Speaking of delving,
I often find it easier to dig into software concepts
by using non-software examples.
It shakes me out of tunnel vision and reinforces software as part of life rather
than a separate and sterile digital realm. So, let’s delve into Delve
.
Common Language is Magical
Magic: The Gathering is a trading card game (similar to others you might have heard of, such as Yu-Gi-Oh! and Pokémon) which acts as an excellent example of the power of keywords encapsulating common language. While there are a core set of rules to Magic: the Gathering (hereafter referred to as MTG), there are also rules printed on many cards that either govern the behaviour of that card or change the broader rules of play. Let’s look at a few examples that do a great job illustrating common language, and then I’ll break down the examples in greater detail:
Looking at the first card, Soulflayer, we see a great example of a keyword
mechanic, Delve
, followed some text explaining what the keyword mechanic
means. You can see from this example that the compression of meaning is
substantial. Normally this explanation text wouldn’t be included, but as it
had been out of use for a long time as of Soulflayer’s printing, the designers
decided players likely needed a refresher. Lower on Soulflayer,
there are examples of other keywords (such as “flying” and “deathtouch”) which,
because of their constant use in the game’s common language, need no such
attached explanation. This is immensely practical, as this card would be
illegible if all 12 of the keywords the card contains were fully spelled out.
To see the extremes of this, we can look at the next two cards in combination,
which for the purposes of play are the same card, all the text of the latter
being represented in the former by the simple keyword, “Plains”.
One of the ongoing challenges in MTG card design is that keywords, once established, are rigid. They must be to support strict rules enforcement. While there are some such keywords that do the equivalent of accepting parameters to modify their function, there are many other cases in which rules still need to be spelled out because they represent rules customization similar to a keyword but that the keyword itself can’t support. Where MTG makes a great example of keywords and their benefits in terms of allowing the greatest design freedom within strict limitations (such as the footprint a physical trading card), its keyword rigidity is not analogous to the customizability of design patterns. It’s also far from the only example of deep meaning encapsulated within keywords. To explore the intersection of keywords and customizability, we need to make another lateral jump.
Customization, Cooking, and Baking
Cooking and baking are a wonderful example of customizability within keywords. Each recipe and ingredient act as keywords, conveying varying levels of meaning depending on one’s familiarity with the dish, with similar dishes, and with ingredients. It’s a domain where customizability is high, but there are still limitations. Some of these limitations are physical, but many more are practical or cultural.
Of these, the physical limitations are plentiful (oven temperature, available ingedients, the square-cube law unfairly limiting the maximum theoretical steak size), but also the least interesting. One important customizability consideration comes from comparing cooking and baking, where, taste aside, the chemical reactions required for proper baking to occur are a large limiting factor, and one that can be daunting for beginner bakers looking to branch out from existing recipes.
How much butter can I melt onto toast before buttered toast becomes butter soup with croutons? This absurd question begs a less absurd question: how much can a recipe be modified before it is no longer recongnizable as the original recipe? Consider that recognizability depends highly on the parties involved, and if you’re speaking to an Italian steeped in cultural tradition, they may be less flexible about what constitutes Carbonara than average.
Having stared into the abyss of butter soup, amidst madness we can find wisdom in approaching the customizability of existing designs with care. Exactly how much customization we apply depends on the environment in which we’re working (butter soup at home, not at the restaurant), and our own familiarity with the given design pattern. As our skills grow, we can better understand what is integral to the design and what can be customized to taste or circumstance. One practical piece of advice for beginners within this framing might be to aim to be a by-the-book baker in the beginning, and keep an open mind to artistic customized cooking opportunities as your skills develop.
Colleagues & Code Reviews
Code review can be a treat if you and your organization let it. At its best it is a plethora of learning opportunities and constructive feedback in every direction (as long as there’s time to go beyond the critical check and the LGTM!). One of the skills within code review that I’ve loved to invest in over the years has been figuring out how to push colleagues to do their best work, and push in a way that avoids frustration, minimizes drag, and encourages the colleague to internalize the desire for a given change rather than feeling it’s been foisted upon them. It’s tricky, and relies on knowledge of the person you’re reviewing as much as it does knowledge of the code.
It was one such occasion with an inquisitive colleague that prompted this teachable moment. It started with a draft PR in which he created a struct-like class, with many private properties, and their getters and setters. Having run into enough software bugs related to mutable objects and wanting to set ourselves up for greatest future maintainability, I pointed out that making the object immutable might be a better strategy. It was a non-blocking, friendly invitation for the developer to push himself and build better habits. At the time I was not in an organization where there was always time to take such opportunities, nor was there always appetite on the part of the developer, so I was thrilled when he reached out to me later that day, having tried to refactor and failed due to the surrounding code he had written. Not only did he consider the feedback, but he attempted to enact it, and upon failure asked for guidance rather than giving up and sticking with the initial strategy. This was a best case scenario.
Building Builders
Towards the goal of making the class immutable, the challenge my colleague had
was that in PHP,
the most well-established version of the builder
instantiates
the product class (the class to be built)
within the constructor (usually via a $this->reset()
method), then uses setters to fulfill each build step. There are a few of
benefits to this approach that make it popular:
- the builder doesn’t hold onto any properties itself
- the builder can be reset easily after the final
$builder->getProduct()
has been called - it’s simple to implement the builder
- the constructor of the product class is kept simple
In scenarios where you’re hydrating product objects by iterating over data sets
that are sparse or generally have risks of inconsistent or null values, this
technique shines, as the easy reset()
allows the same builder to be used
between iterations.
Beyond the mutability of the product object, there aren’t
too many downsides, but immutability is widely regarded as best practice, as it
leads to the lowest risk during object reuse and allows the system to enact
concurrent processes safely. Especially in PHP code
written after version 8, the combination of constructor parameter typing and
the ordering of required fields before optional fields mean that the constructor
can be useful as an easy visual inspection of an immutable class’s properties
and how much one can trust them to have the given property. That is lost in
all mutable classes, but is further lost with this particular builder pattern,
since,
while it keeps the constructor of the product class simple, it typically
requires that the constructor have no parameters at all. This is not only
misleading, but will likely require additional error handling for missing
required fields, and extra null checks within all the code that uses the
product. Those are some major tradeoffs that clutter up a codebase quickly.
Variants
Starting from the most common version, it’s time to refactor! Before we do so, it is likely valuable to save existing work. Assuming we’re using a version control tool such as Git, if we don’t like where we end up we can always throw away our changes.
For both variants I’ll cover, the first step is to make the product class immutable. Since this is the design goal that instigated the refactor, all other code can warp around this change.
I’ll use a trivial class for the following examples. Let’s both pretend the product under construction is expanded to sufficient complexity that it actually warrants a builder (which this example as written would not).
Note on code spacing: some lines have likely been broken up in ways you find odd. This is due to an Astro styling issue with code blocks that I have yet to resolve, but am working on! For now, please forgive my unorthodox line breaks!
So, from:
class Foo {
private int $bar;
private string $baz;
private string $optionalOne;
public function __construct() {}
//getters...
//setters...
}
to:
class Foo {
private int $bar;
private string $baz;
private string $optionalOne;
public function __construct(
int $bar,
string $baz,
string $optionalOne = ''
) {
$this->bar = $bar;
$this->baz = $baz;
$this->optionalOne = $optionalOne;
}
//getters...
//NO SETTERS
}
Easy! There is also one change to the builder that must happen because of this,
being that the builder will have to maintain the state of these properties until
the getProduct()
method has been called. With that in mind, the design
challenge become:
- Does the builder still reset? If so, how?
- How does the builder handle required properties?
From here, we can dig into two builder variants that work with immutable product classes:
Variant 1
class FooBuilder {
private int $bar;
private string $baz;
private string $optionalOne;
public function __construct() {}
public function withBar(int $bar): void {
$this->bar = $bar;
}
public function withBaz(string $baz): void {
$this->baz = $baz;
}
public function withOptionalOne(
string $optionalOne
): void {
$this->optionalOne = $optionalOne;
}
public function reset(): void {
$this->bar = null;
$this->baz = null;
$this->optionalOne = '';
}
public function getProduct(): Foo {
if(
!isset($this->bar) ||
!isset($this->baz)) {
throw new InvalidArgumentException(
'Required property missing'
);
}
$foo = new Foo(
$this->bar,
$this->baz,
$this->optionalOne
);
$this->reset();
return $foo;
}
}
Pros:
- Product class immutability is supported
- This structure maintains high method signature similarity to the traditional builder. Anyone familiar with the traditional builder will recognize this intent of this class.
- the
reset()
could optionally be made private, simplifying the usage of the builder. - works well for building multiple products using iteration
Cons:
- By hiding the required properties, we’re exposing risk that the class will be used incorrectly
- We’ve complicated the code with conditionals
- We’re throwing an exception
This is a pretty good option! It’s especially important that it works well for multiple builds, and that other developers will be able to recognize it and use it easily. Still, the fact that it hides what is required is not ideal, and I do like to avoid exception cases in places where I can instead direct the upstream coder towards the correct behaviour instead. So, let’s try that.
Variant 2
class FooBuilder {
private int $bar;
private string $baz;
private string $optionalOne;
public function __construct(
int $bar,
string $baz
) {
$this->bar = $bar;
$this->baz = $baz;
}
public function withOptionalOne(
string $optionalOne
): void {
$this->optionalOne = $optionalOne;
}
public function reuseBuilder(
int $bar,
string $baz
): void {
$this->bar = $bar;
$this->baz = $baz;
$this->optionalOne = '';
}
public function getProduct(): Foo {
$foo = new Foo(
$this->bar,
$this->baz,
$this->optionalOne
);
return $foo;
}
}
Pros:
- No exception thrown
- No conditionals
- It is clear to the user of the builder which properties are required
- No “with” methods for required properties
- mirrors reality (I’ll explain below)
getProduct()
doesn’t need to reset
Cons:
- Calling
getProduct()
no longer resets, and allowing it to callreuseBuilder()
would be messy and unintuitive. - Calling
reuseBuilder($bar, $baz)
is not as familiar asreset()
, but leaving a traditionalreset()
in place would invalidate the required fields. - Not as intuitive within an iterator.
The cons of this customization are more significant than the cons of the first,
but so are the benefits. Part of the cons fall into the category of “bad because
they’re unfamiliar to other devs”, and that largely becomes a judgement call
about the adaptability of the rest of the development team, and about the
surrounding
coding practices in your codebase. In this example I used the suffix
Builder
on FooBuilder
not only because it’s clear, but also because adding
such suffixes to classes that implement design patterns was allowed in the
codebase in which we were working. Naming classes with design pattern suffixes
allows greater customization while
still supporting shared language of design pattern keywords, and it also alerts
code reviewers to a coder’s intent, which in turn allows reviewers
to question and correct
in scenarios where the coder cuztomizes the pattern out of
reasonable recognition.
For me, one of the coolest benefits of this approach is that it mirrors reality, in the sense that complex constructions with optional parts usually have a base and options, rather than relying solely on options for the entire build. For example, adding a finished basement to a house might be an option when choosing the build, but basement walls are likely a requirement. By codifying this in the builder’s constructor, you’re saying “all the mandatory stuff is out of the way, now you know for sure your house is structurally sound and you can focus on decor.”
Conclusion
Building builders, but is it actually better? That depends on circumstance. If you’re looking to use the builder pattern with an immutable product, you now have two different ways to do that. That’s handy, but it’s the least important part of this exploration.
When my colleague asked for implementation help, he was blocked by an assumed rigidity to the design pattern he had already implemented. All he really needed was a nudge to think of design patterns as customizable. This was the real gold, and it applies to any design pattern, and it is a great lesson to learn at any stage in your software journey.
Ditch the self-imposed shackles, begin with a baker’s mindset as you learn the chemical limitations, and over time your expertise will trend towards a chef’s creativity and flexibility. Enjoy all the flavours along the way!