Builder pattern: A pragmatic approach
03 January 2023
I have been using the Builder pattern a lot recently but when I came across some articles about it, they confused me, so I got the inspiration write a post about it.
The Builder design pattern is applied when many optional attributes are required to initialize a class.
It results in a very expressive way to customize the different fields of a class.
The article illustrates a pragmatic approach to the pattern, implemented in C++.
Three variants are presented, with the first one being so basic that some may not even qualify it as a Builder.
The last one is more “traditional” yet still relatively simple.
If you want to read more about the Builder pattern to brush up your knowledge feel free to check out some
good reading material ([refactoring.guru],
[Vishal Chovatiya])
and then continue with this post.
All code can be found on GitHub and the tutorial also exists as a video that you can view below:
I will assume that you are somewhat familiar with the pattern, so I will mostly focus on its practical implementation. It’s also good if you have some basic understanding of C++. This includes classes, references, and smart pointers. Nothing too much, but we won’t spend any time explaining them.
Let’s start by taking a brief look at the problem this design pattern is trying to solve.
The Builder pattern is often applied when there is a large number of attributes needed to initialize a class.
Specifically, when many or most of these attributes are optional, creating an object may become complex or inconvenient.
In such a case, one “bad” solution would be to have a constructor that takes all possible arguments.
However, the larger the number of attributes, the more arguments the constructor needs to take, resulting in
ever-increasing complexity.
To make things worse, passing all arguments does not make sense in all usages of an instance.
For example, if A
, B
, C
, and D
are possible attributes of our class, one user may only care about attributes B
and D
,
while another user, cares only about D
.
Another inconvenient way to solve this issue is to create multiple constructors which satisfy all the different permutations
that users may want. Again, this does not scale very well as the number of attributes grows.
A final way to solve this without the Build pattern, is to wrap all attributes in another class,
which has some default values, and inject it into the class we want to customize.
This is not a bad solution and could be fine to adopt in several contexts, however, it may be less expressive and
slightly more inconvenient compared to what you will see soon.
The Builder pattern aims to resolve these issues by splitting initialization into multiple steps where the last step
returns the “complete” object.
This approach also allows for a higher degree of expressiveness compared to constructors taking a large number of arguments.
In many examples, you will also see a Director
class that encapsulates the different build steps altogether.
Disclaimer
To focus on the pattern itself, please treat any code you see from now on as pseudocode.
It should compile but I might skip a good practice or two, to keep everything compact.
I will assume C++20 is being used, however, everything you see should be C++11 compatible.
I do not follow any particular coding guidelines, there’s room for optimization and I don’t take any specific domain restrictions
into consideration.
The use case
Before we begin, let’s describe the use case:
We need to create a library that represents menus in some user interface.
All menus must have some kind of “ID” and aside from that, they may have other attributes
such as a title
, a border
in pixels, several menu options
, or an orientation
.
If an optional attribute is not selected a default value will be chosen instead.
Let’s take a look at how we could implement this without the Builder pattern.
Without the Builder pattern
The first, and perhaps most straightforward way, would be a constructor that accepts arguments for all the attributes that need to be initialized.
class Menu
{
public:
// Constructr with all fields
Menu(std::string id,
std::string title,
std::vector<std::string> options,
bool horizontal,
int border);
private:
std::string mId{};
std::string mTitle{};
std::vector<std::string> mOptions{};
bool mHorizontal{false};
int mBorder{0};
};
In this particular case, where we only have 4 optional arguments, it’s probably not that bad, but as the number of attributes rises so do the arguments. Using this approach, we get a constructor that continuously changes with the introduction of new optional attributes and has the potential to become very complicated. Furthermore, there’s no way for a user to avoid passing an argument they don’t specifically care for. We could of course set default values, however, since in C++ we don’t have named arguments, this would eventually be inadequate.
class Menu
{
// Constructors with all combinations of fields
Menu(std::string id);
Menu(std::string id, std::string title);
Menu(std::string id, std::vector<std::string> options);
// ...
};
A way around this would be multiple constructors with all combinations a user would want. This however has the potential to “explode”, the more fields are added. So unless you have a very small number of optional fields, let’s say less than 3, you should not consider this approach.
struct MenuOptions
{
std::string title{"Some default title"};
std::vector<std::string> options{};
bool horizontal{false};
int border{0};
};
class Menu
{
public:
Menu(std::string id, MenuOptions options = MenuOptions{});
};
Moreover, we could wrap all the optional attributes in a separate class MenuOptions
.
MenuOptions
has some default values set and is passed as an argument to the class we want to customize.
I consider this approach a great alternative to the builder pattern. It may lack some expressiveness to my eyes, but that’s really
subjective.
class Menu
{
Menu(std::string id);
void setTitle(std::string title);
void setOptions(std::vector<std::string> options);
void setHorizontal(bool horizontal);
void setBorder(int border);
};
Finally, you may have setters for each field and the user can call whichever they wish to initialize the respective field.
This is fine and it is, considering the circumstances, the best alternative to the Builder pattern.
As far as I am concerned, this approach only lacks the expressiveness and the inability to declare the Menu
instance as constant.
Now let’s see how we would do things using the Builder pattern instead.
Basic builder
I’ll start with this very basic implementation that is so basic that some may not even consider it a Builder pattern.
class Menu
{
public:
static Menu create(std::string id)
{
return Menu{id};
}
void show() const {}
void select(int /* index */) const {}
Menu& withTitle(std::string title)
{
mTitle = title;
return *this;
}
Menu& withBorder(int pixels)
{
mBorder = pixels;
return *this;
}
Menu& addOption(std::string option)
{
mOptions.emplace_back(option);
return *this;
}
Menu& horizontal()
{
mHorizontal = true;
return *this;
}
Menu& vertical()
{
mHorizontal = false;
return *this;
}
private:
std::string mId{};
std::string mTitle{};
std::vector<std::string> mOptions{};
bool mHorizontal{false};
int mBorder{0};
Menu(std::string id)
: mId{id}
{
}
};
We start with a static create
method that takes all compulsory arguments and returns a Menu
instance
by invoking the class’ private constructor.
This way we force our users to use this method for creating an instance.
In this particular case, the “create”() method is not very useful, so you could skip it, make the constructor public,
and use it directly.
Its main advantage is that it can “hide” some construction logic and that it may make usage more readable,
but this last one is ultimately a personal preference.
Anyway, let’s move on with the “setter” methods which do two things:
(a) set some member variable and (b) return a reference of the current class.
This technique allows us to “chain” multiple calls for customizing the different attributes as we see fit in each use case.
const auto mainMenu = Menu::create("main")
.withTitle("Main Menu")
.addOption("Option 1")
.addOption("Option 2")
.horizontal();
mainMenu.show();
mainMenu.select(1);
const auto bottomMenu = Menu::create("bottom")
.addOption("Option 1")
.addOption("Option 2")
.addOption("Option 3")
.withBorder(2);
For example, mainMenu
should have a title, two options, and should be horizontal,
while the bottomMenu
should have three options and a border of two pixels.
Some may say this is not a “proper” Builder pattern because the user may get a usable Menu
object
before they “finish” setting the various attributes.
While I agree, the goal of applying design patterns is to solve recurring problems and not to create new ones,
for example, complexity, just to follow a textbook example.
So if this implementation fixes the issue of having multiple optional attributes, then by all means go for it.
But let’s take a look at what a more “traditional”, yet still simple, Builder pattern would look like.
Traditional builder
We will need two classes, a “builder” class, and an “implementation” class.
Here I have chosen a Menu
as the builder class and the nested class Impl
as the implementation.
class Menu
{
public:
class Impl
{
public:
void show() const {}
void select(int /* index */) const {}
private:
friend class Menu;
std::string mId{};
std::string mTitle{};
std::vector<std::string> mOptions{};
bool mHorizontal{false};
int mBorder{0};
Impl(std::string id)
: mId{id}
{
}
};
Menu(std::string id)
: mImpl{new Impl{id}}
{
}
std::unique_ptr<Impl> build()
{
return std::move(mImpl);
}
Menu& withTitle(std::string title)
{
mImpl->mTitle = title;
return *this;
}
Menu& withBorder(int pixels)
{
mImpl->mBorder = pixels;
return *this;
}
Menu& addOption(std::string option)
{
mImpl->mOptions.emplace_back(option);
return *this;
}
Menu& horizontal()
{
mImpl->mHorizontal = true;
return *this;
}
Menu& vertical()
{
mImpl->mHorizontal = false;
return *this;
}
private:
std::unique_ptr<Impl> mImpl{};
};
The two classes don’t have to be nested, however, you will want the builder class to be declared
as a friend
of the implementation class so that it can access its private members.
The idea is that the builder class, Menu
, creates an instance of the implementation and sets the various attributes.
It will only relinquish the “implementation” ownership as soon as the user signifies
that they are done with initializing the instance.
This is done by calling the build()
method, the “last step” of the chained call.
Let’s take a closer look. The Impl
class is the one that would contain the Menu-related business logic.
In this case, the show
and select
functions, as well as the various fields.
On the other hand, the Menu
class follows the technique we saw before, with methods returning a reference to the instance.
First, an Impl
instance is created in the Menu
constructor and then the different member functions customize
the respective private fields of the Impl
instance.
When the user is done with initialization, then they should call build()
which would return the “fully assembled” instance
with all the properties correctly set.
Note that the Impl
constructor is private to “force” construction via the Builder pattern
and this is why we cannot use std::make_unique
when creating the pointer to it.
This is the more “traditional” implementation of the Builder pattern,
where the user gets access to the built object only after they have finished initializing it.
const auto menu = Menu{"main"}
.withTitle("Main Menu")
.withBorder(1)
.addOption("Option 1")
.addOption("Option 2")
.horizontal()
.build();
menu->show();
menu->select(1);
In the end, the usage looks very similar to what we previously saw.
The differences are that the build()
function needs to be called at the end and that the type of the
menu
instance is a unique pointer of Menu::Impl
and not Menu
.
Depending on the use case, you may even skip the unique pointer for the return type of the build()
function.
Builder with inheritance
The third way of implementing the “Builder” pattern is essentially the same as the previous one, except that all classes now implement an abstract interface. This variant of the pattern is closer to the textbook examples you will find online, especially in other languages.
class Menu
{
public:
virtual ~Menu() = default;
virtual void show() const = 0;
virtual void select(int index) const = 0;
};
class Builder
{
public:
virtual ~Builder() = default;
virtual Builder& withTitle(std::string title) = 0;
virtual Builder& withBorder(int pixels) = 0;
virtual Builder& addOption(std::string option) = 0;
virtual Builder& horizontal() = 0;
virtual Builder& vertical() = 0;
virtual std::unique_ptr<Menu> build() = 0;
};
We first start by defining the abstraction for what we have been calling so far “implementation”.
Let’s call it Menu
.
It exposes the business logic via the show()
and select()
functions.
Then, we create the Builder
interface.
This one exposes the different ways an implementation object can be customized.
Similar to before, we also have a build()
function that returns an instance that implements the interface that offers your business logic, a unique_ptr
of the Menu
.
const auto menu = MenuBuilder{"main"}
.withTitle("Main Menu")
.withBorder(1)
.addOption("Option 1")
.addOption("Option 2")
.horizontal()
.build();
menu->show();
menu->select(1);
The usage is essentially the same as before. However, now you have the possibility to depend on an interface instead of a concrete class. This allows you seamlessly switch among different “Builder” and “Menu” implementations or inject a mock.
class MenuBuilder : public Builder
{
public:
class DefaultMenu : public Menu
{
public:
void show() const override {}
void select(int /* index */) const override {}
private:
friend class MenuBuilder;
std::string mId{};
std::string mTitle{};
std::vector<std::string> mOptions{};
bool mHorizontal{false};
int mBorder{0};
DefaultMenu(std::string id)
: mId{id}
{
}
};
MenuBuilder(std::string id)
: mMenu{new DefaultMenu{id}}
{
}
std::unique_ptr<Menu> build() override
{
return std::move(mMenu);
}
Builder& withTitle(std::string title) override
{
mMenu->mTitle = title;
return *this;
}
Builder& withBorder(int pixels) override
{
mMenu->mBorder = pixels;
return *this;
}
Builder& addOption(std::string option) override
{
mMenu->mOptions.emplace_back(option);
return *this;
}
Builder& horizontal() override
{
mMenu->mHorizontal = true;
return *this;
}
Builder& vertical() override
{
mMenu->mHorizontal = false;
return *this;
}
private:
std::unique_ptr<DefaultMenu> mMenu{};
};
The implementation looks almost the same as before.
I have changed the names a bit to reflect the usage of the abstractions.
Now the outer class is called MenuBuilder
and implements the Builder
interface,
while the nested “implementation” class is called DefaultMenu
and implements Menu
.
No other substantial changes have taken place.
Please note that returning a unique pointer from the build()
function, in this case,
is something you will not be able to avoid.
To summarize, the Builder pattern may help your users write simpler and more expressive code
when creating objects that have a large number of optional attributes.
Keep in mind that your primary driver should be simplicity,
both for your users but also for yourself as the one implementing a library.
Therefore, avoid over-engineering things, adopt the pattern when it makes sense, and apply it in a reasonable way.