Builder Pattern in C++, the Right Way

Ant Wang
10 min readAug 22, 2023

--

Photo by Marcel Strauß on Unsplash

There are many ways people interpret Builder Design Pattern. The core of the Builder Pattern is to replace a constructor call overfilled with parameters with a step by step instruction on how to build and assemble the product. For example, instead of instantiating a burger object by calling it’s parameterized constructor:

Burger(BurgerBun, BeefPatty, Onions, Lettuce, Cheese, Ketchup, Mayo);

we decompose the process into steps to construct the burger such as:

Burger.AddBread()
Burger.AddProtein()
Burger.AddSides()
Burger.AddSauce()

When creating the object, the client just need to focus on calling the Builder and let it take care of the instantiation. Sounds simple, doesn’t it? But I think the Builder Pattern goes beyond this simple instruction decomposition. Just like the previous article on Factory Method, I want to share what I learned from Refactoring Guru and summarize the comprehensive definition of Builder Design Pattern.

Photo by Eaters Collective on Unsplash

Let’s Start without any design pattern and open up a sandwich shop. Let’s assume that Burgers, Hotdogs, and BLT are all types of sandwiches. Even though they’re all sandwiches, but the composition of the sandwich is different. This can be represented with a Sandwich class.

enum class Bread {Toast, HotdogBun, BurgerBun};
enum class Protein {BeefPatty, Dog, Bacon};
enum class Side{Lettuce, Tomato, Onions, Cheese};
enum class Sauce {Mayo, Ketchup, YumYum, Mustard};
enum class Wrapper {Paper, Box, Plastic};

class Sandwich {
public:
Sandwich(Bread b, Protein p, std::vector<Side> sides, std::vector<Sauce> sauces, Wrapper w) :
b{b}, p{p}, sides{sides}, sauces{sauces}, w{w} {
std::cout << "A Sandwich is Constructed" << std::endl;
}
Bread b;
Protein p;
std::vector<Side> sides;
std::vector<Sauce> sauces;
Wrapper w;
};

Now, to construct a sandwich, we would need to pass in all the different parameters to make the specific type of sandwich:

int main() { 

Sandwich burger(Bread::BurgerBun, Protein::BeefPatty,
std::vector<Side>{Side::Lettuce,Side::Cheese,Side::Tomato},
std::vector<Sauce>{Sauce::YumYum}, Wrapper::Paper);

Sandwich hotdog(Bread::HotdogBun, Protein::Dog,
std::vector<Side>{Side::Onions},
std::vector<Sauce>{Sauce::Ketchup, Sauce::Mustard}, Wrapper::Box);

Sandwich BLT(Bread::Toast, Protein::Bacon,
std::vector<Side>{Side::Tomato, Side::Lettuce},
std::vector<Sauce>{Sauce::Mayo}, Wrapper::Plastic);

return 0;
}

This is extremely tedious, error-prone, and manual way of doing things. The client may accidentally select the wrong component, and this code would still compile and run. But the customer would end up with a hotdog with a beef patty and mayo inside. Let’s try to improve it by introducing the Builder Pattern.

Now, we’re getting into the first key element of a Builder Pattern — creating a Builder class that takes care of the step by step construction.

class SandwichBuilder {
public:
virtual void AddBread() = 0;
virtual void AddProtein() = 0;
void AddSide(Side s) {product.sides.push_back(s);}
void AddSauce(Sauce s) {product.sauces.push_back(s);}
virtual void AddWrapper() = 0;
Sandwich ReturnProduct() {return product;} // Note that this should be decalred in concrete builders if returned products are different.
Sandwich product;
};

There will be certain methods that are universal despite of the type of sandwich (base class methods), and certain methods that is unique to the type of sandwich (overriding the virtual functions). For example, adding bread to the sandwich would be type specific because burgers use sesame buns while BLTs use toast. In our case, the AddBread() is declared as a pure virtual function so the child class, or Concrete Builder, can freely implement their own definition of AddBread(). On the other hand, ReturnProduct is universal to all extended classes, since the only job of the function is to return the built sandwich. In that case it would be implemented in the Base class.

Though AddSide() can also be implemented by the child classes, I intentionally made AddSide() a base class method. You’ll see why in a second.

Now that we have the SandwichBuilder Interface built out, let’s build our concrete Builders: BurgerBuilder, HotdogBuilder, and BLTBuilder.

class BurgerBuilder : public SandwichBuilder {
public:
void AddBread() {
std::cout << "Heating up the Seasame Sprinked Bun" << std::endl;
product.b = Bread::BurgerBun;
}
void AddProtein() {
std::cout << "Grilling the Wagyu" << std::endl;
product.p = Protein::BeefPatty;
}
void AddWrapper()) {
std::cout << "Wrapping with 100% recycled paper" << std::endl;
product.w = Wrapper::Paper;
}
};

class HotdogBuilder : public SandwichBuilder {
public:
void AddBread() {
std::cout << "Steaming up the buns" << std::endl;
product.b = Bread::HotdogBun;
}
void AddProtein() {
std::cout << "Boiling the hotdog in brine" << std::endl;
product.p = Protein::Dog;
}
void AddWrapper() {
std::cout << "Placing hotdog into box" << std::endl;
product.w = Wrapper::Box;
}
};

class BLTBuilder : public SandwichBuilder {
public:
void AddBread() {
std::cout << "Toasting bread in toaster" << std::endl;
product.b = Bread::Toast;
}
void AddProtein() {
std::cout << "Sizzling bacon in cast iron pan" << std::endl;
product.p = Protein::Bacon;
}
void AddWrapper() {
std::cout << "Wrapping up in a decompostable plastic wrap" << std::endl;
product.w = Wrapper::Plastic;
}
};

Now, to create the burgers, we no longer need to call the long and lengthy constructors. We can use Builder’s directly to step by step compose the Sandwich’s. And of course, we can create helper functions that wrap the step by step process for the client code to use:

Sandwich BuildBurger() {
BurgerBuilder builder;
builder.AddBread();
builder.AddProtein();
builder.AddSide(Side::Lettuce);
builder.AddSide(Side::Tomato);
builder.AddSauce(Sauce::YumYum);
builder.AddWrapper();
return builder.ReturnProduct();
}

Sandwich BuildHotdog() {
HotdogBuilder builder;
builder.AddBread();
builder.AddProtein();
builder.AddSide(Side::Onions);
builder.AddSauce(Sauce::Ketchup);
builder.AddSauce(Sauce::Mustard);
builder.AddWrapper();
return builder.ReturnProduct();
}

Sandwich BuildBLT() {
BLTBuilder builder;
builder.AddBread();
builder.AddProtein();
builder.AddSide(Side::Lettuce);
builder.AddSide(Side::Tomato);
builder.AddSauce(Sauce::Mayo);
builder.AddWrapper();
return builder.ReturnProduct();
}

int main() {
Sandwich burger = BuildBurger();
Sandwich hotdog = BuildHotdog();
Sandwich BLT = BuildBLT();
return 0;
}

This way, client code only needs to worry about calling the right Builder type and the object creation is taken care automatically.

By the way, an interesting technique we can deploy here is called Method Chaining. I find this method quite interesting and it makes our code more compact, though arguably less easy to navigate. The idea of Method Chaining is that instead of having separate lines of code like this:

builder.AddBread();
builder.AddProtein();
builder.AddSide(Side::Onions);
builder.AddSauce(Sauce::Ketchup);
builder.AddSauce(Sauce::Mustard);
builder.AddWrapper();

With a few tweaks, we chain it into one single expression:

builder.AddBread().AddProtein().AddSide(Side::Onions).AddSauce(Sauce::Ketchup).AddSauce(Sauce::Mustard).AddWrapper();

Cool isn’t it? The trick here is that in each of the method, we need to return an object reference so that the expression can continue to be chained onwards. In other words, builder.AddBread() returns the same builder object we just used, and we can continue to invoke it’s next method AddProtein(). Here’s an example of writing the BurgerBuilder class and the BuildBurger function using Method Chaining:

class BurgerBuilder : public SandwichBuilder {
public:
SandwichBuilder& AddBread() {
std::cout << "Heating up the Seasame Sprinked Bun" << std::endl;
product.b = Bread::BurgerBun;
return *this;
}
SandwichBuilder& AddProtein() {
std::cout << "Grilling the Wagyu" << std::endl;
product.p = Protein::BeefPatty;
return *this;
}
SandwichBuilder& AddWrapper()) {
std::cout << "Wrapping with 100% recycled paper" << std::endl;
product.w = Wrapper::Paper;
return *this;
}
};

Sandwich BuildBurger() {
BurgerBuilder builder;
builder.AddBread().AddProtein().AddSide(Side::Lettuce).AddSide(Side::Tomato).AddSauce(Sauce::YumYum).AddWrapper();
return builder.ReturnProduct();
}

The function BuildBurger() could certainly be a method of the BurgerBuilder class instead of a a helper function. This way, the client code simply needs to ask BurgerBuilder to BuildBurger() and ReturnProduct(), and a tasty burger will be ready to be enjoyed.

Here we have it — Concrete Builders that handles the construction of objects that would otherwise take a long list of parameters in the construction call. This is where people think they’ve got Builder pattern under their belt, but let’s not stop here.

We’re not really leveraging the full potential of OOP. What if we want a modified version of the burger with no sauce? What if we want a dine-in order with no wrapper? This code is not scalable yet, and the only benefit we’re bringing to the table is a simplified object creation process.

Before going deeper, let’s look at the definition from Refactoring Guru

Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.

An aspect that is missing from the previous example is that the construction for the code is different across the different SandwichBuilder extensions. The BurgerBuilder only has one sauce and two sides added to the sandwich while the HotdogBuilder has two sauce and one side added. That makes the construction step slightly varied and cannot be reused at scale because the SOP for BurgerBuilder involves calling AddSauce(YumYum), AddSide(Lettuce), AddSide(Tomato), while HotdogBuilder calls AddSauce(Ketchup), AddSauce(Mustard), AddSide(Onions).

To fix this, let’s align the steps of building sandwich into:

  1. AddBread: (Same a before)
  2. AddProtein: (Same a before)
  3. LoadSides: A Wrapper Function that adds all the sides
  4. DrizzleSauce: A Wrapper Function that adds all the sauces
  5. AddWrapper: (Same a before)

With this change implemented the Sandwich Building process is standardized across all types of sandwiches, whether it is Burger, BLT, or Hotdog. Here’s what the Builder classes look like now:

class SandwichBuilder {
public:
virtual void AddBread() = 0;
virtual void AddProtein() = 0;
void AddSide(Side s) {product.sides.push_back(s);}
void AddSauce(Sauce s) {product.sauces.push_back(s);}
virtual void AddWrapper() = 0;
virtual void LoadSides() = 0;
virtual void DrizzleSauces() = 0;
Sandwich ReturnProduct() {return product;} // Note that this should be decalred in concrete builders if returned products are different.
Sandwich product;
};

class BurgerBuilder : public SandwichBuilder {
public:
void AddBread() {
std::cout << "Heating up the Seasame Sprinked Bun" << std::endl;
product.b = Bread::BurgerBun;
}
void AddProtein() {
std::cout << "Grilling the Wagyu" << std::endl;
product.p = Protein::BeefPatty;
}
void AddWrapper() {
std::cout << "Wrapping with 100% recycled paper" << std::endl;
product.w = Wrapper::Paper;
}
void LoadSides(){
std::cout << "Loading up the Burger" << std::endl;
this->AddSide(Side::Lettuce);
this->AddSide(Side::Tomato);
}
void DrizzleSauces(){
std::cout << "Drizzling the sauces" << std::endl;
this->AddSauce(Sauce::YumYum);
}
};

class HotdogBuilder : public SandwichBuilder {
public:
void AddBread() {
std::cout << "Steaming up the buns" << std::endl;
product.b = Bread::HotdogBun;
}
void AddProtein() {
std::cout << "Boiling the hotdog in brine" << std::endl;
product.p = Protein::Dog;
}
void AddWrapper() {
std::cout << "Placing hotdog into box" << std::endl;
product.w = Wrapper::Box;
}
void LoadSides(){
std::cout << "Loading up the Hotdog" << std::endl;
this->AddSide(Side::Onions);
}
void DrizzleSauces(){
std::cout << "Drizzling the sauces" << std::endl;
this->AddSauce(Sauce::Ketchup);
this->AddSauce(Sauce::Mustard);
}
};

class BLTBuilder : public SandwichBuilder {
public:
void AddBread() {
std::cout << "Toasting bread in toaster" << std::endl;
product.b = Bread::Toast;
}
void AddProtein() {
std::cout << "Sizzling bacon in cast iron pan" << std::endl;
product.p = Protein::Bacon;
}
void AddWrapper() {
std::cout << "Wrapping up in a decompostable plastic wrap" << std::endl;
product.w = Wrapper::Plastic;
}
void LoadSides(){
std::cout << "Loading up the BLT" << std::endl;
this->AddSide(Side::Lettuce);
this->AddSide(Side::Tomato);
}
void DrizzleSauces(){
std::cout << "Drizzling the sauces" << std::endl;
this->AddSauce(Sauce::Mayo);
}
};

To make the different burgers, we now have a standard interface to call, and all we need to do is pass in the correct type of SandwichBuilder.

Sandwich CreateSandwich(SandwichBuilder* builder) {
builder->AddBread();
builder->AddProtein();
builder->LoadSides();
builder->DrizzleSauces();
builder->AddWrapper();
return builder->ReturnProduct();
}

int main(){
CreateSandwich(new BurgerBuilder());
CreateSandwich(new HotdogBuilder());
CreateSandwich(new BLTBuilder());
}

Note that all we did was to find the common steps amongst the builder implementations and extract them into the common SandwichBuilder interface, and it’s already made the code much more scalable.

Director

Photo by Jakob Owens on Unsplash

The scalability of our Sandwich shop will amplify when we introduce the Director class. According to Refactoring Guru, here is the definition of Director:

You can go further and extract a series of calls to the builder steps you use to construct a product into a separate class called director. The director class defines the order in which to execute the building steps, while the builder provides the implementation for those steps.

It is basically a “builder-agnostic” class that only cares about executing the pre-defined construction steps. Whatever builder we give the Director, the director will focus on executing the construction steps and returning the product. In other words, we can have a recipe or a manifest for scenarios such as:

  • Dine In Order: AddBread -> AddProtein -> LoadSides -> DrizzleSauces
  • Takeout Order: Steps from Dine In Order -> AddWrapper
  • Order No Sides: AddBread -> AddProtein -> DrizzleSauces
  • Order without Sauces: AddBread -> AddProtein -> LoadSides

Here is the implementation of the Director class:

class Director {    // 
public:
SandwichBuilder* builder;
void SetBuilderType(SandwichBuilder* builder){
this->builder = builder;
}
Sandwich TakeoutOrder() {
builder->AddBread();
builder->AddProtein();
builder->LoadSides();
builder->DrizzleSauces();
builder->AddWrapper();
return builder->ReturnProduct();
}

Sandwich DineInOrder() {
builder->AddBread();
builder->AddProtein();
builder->LoadSides();
builder->DrizzleSauces();
return builder->ReturnProduct();
}

Sandwich NoSauce() {
builder->AddBread();
builder->AddProtein();
builder->DrizzleSauces();
return builder->ReturnProduct();
}

Sandwich NoSides() {
builder->AddBread();
builder->AddProtein();
builder->DrizzleSauces();
return builder->ReturnProduct();
}
};

And to make all the different possible orders from the sandwich shop, we can call:

int main() {
Director d;
Sandwich s;

// Make Burger Orders
d.SetBuilderType(new BurgerBuilder());
s = d.TakeoutOrder();
s = d.DineInOrder();
s = d.NoSauce();
s = d.NoSides();

// Make Hotdog Orders
d.SetBuilderType(new HotdogBuilder());
s = d.TakeoutOrder();
s = d.DineInOrder();
s = d.NoSauce();
s = d.NoSides();

// Make BLT Orders
d.SetBuilderType(new BLTBuilder());
s = d.TakeoutOrder();
s = d.DineInOrder();
s = d.NoSauce();
s = d.NoSides();
return 0;
}

Whenever there is a change in the construction steps, we just need to change the Director class. Whenever there’s change to the actual implementation of the burgers, we change the Builder class. This makes our code a lot more scalable and abstracts the construction process from the client code.

We examined a simple version of Builder class that only encapsulates the construction process to eliminate “telescoping” constructors, but doesn’t provide scalability because the Builder class did not provide a unified object construction process. The steps to create a BLT is different from the steps to create a Hotdog.

However, once we fully extracted the common building steps across the different sandwiches and implemented it into the Builder sub & super classes, and introduced the Director to orchestrate our build manifest, we are able to unleash the full power of Design Patterns and OOP using the Builder method.

--

--

Ant Wang

Self taught software developer passionate for robotics and industrial automation.