Builder pattern: A pragmatic approach

03 January 2023

github copilot cpp

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.