Originally posted here
The Builder is a creational design pattern that allows us to
construct complex objects step by step. The pattern separates an object from its representation so that the same construction process can create different representations.
The Builder is a handy tool when we have a collection of pieces for a complex object and need to build them all together. With this pattern, we can initialize each piece of the complex object step by step and then build them all up to a complete object.
You can find the example code of this post, on Github
Conceptualizing the Problem
Imagine a complex object that requires step-by-step initialization of many fields, some of them optional. Such initialization code is usually either buried under a monstrous constructor with lots of parameters or scattered around numerous constructors that only initialize some fields.
Let's think about how to create a Pizza
object. To build a simple pizza, we need to add a simple dough, some tomato sauce and shredded cheese. But what if we want to build a bigger, better pizza, with cream cheese in the crust, and other goodies (like pepperoni, bell peppers, and mozzarella cheese)?
The simplest solution is to extend the base Pizza
class and create a set of subclasses to cover all combinations of the parameters. But eventually, you'll end up with a considerable number of subclasses. Any new parameter will require growing this hierarchy even more.
There's another approach that doesn't evolve creating more subclasses. We can create a giant constructor right in the base Pizza
class with all possible parameters that control the pizza object. While this approach indeed eliminates the need for multiple subclasses, it creates another problem.
In most cases, the majority of parameters will be unused, making the constructor calls pretty ugly. For instance, only a fraction of pizzas have pineapple as a topping, so the parameters related to it will be useless most of the time.
The Builder pattern suggests that we extract the object construction code out of this own class and move it to a separate object called the builder.
The pattern organizes object construction into a distinct set of steps (AddPepperoni
, AddBellPeppers
, etc.). To create a new object, we just need to call only the necessary steps for producing a particular configuration of an object.
The Director
We can go further and extract a series of calls to the builder steps used to build a product, into a separate class called th*e director*. The director class defines the order in which to execute the building steps, while the builder provides the implementation of those steps.
Having a director is not necessary. We can always call the building steps in a specific order directly from the client code. However, the director class might be useful for having various construction routines so that they can be reused across the code.
Moreover, the director class hides the details of product construction from the client code. The code only needs to associate a builder with a director, launch the construction, and get the results from the builder.
Structuring the Builder Pattern
The following diagram represents the different builder parts.
The pattern has the following parts:
- Director: The Director is a class that decides in what steps should the complex class be built. In this example, the name of the complex object is Product. It doesn't build the Product class directly, but instead, it calls the IBuilder interface to create the parts.
- IBuilder: The IBuilder interface defines all the necessary methods to build a Product class. These methods are common to all implementations of the Builder. The Director calls the Build method to get the result of the construction.
- ConcreteBuilder: Implements the IBuilder interface. It takes a Product class and incrementally builds it.
- Product: The Product object is a complicated class that we need to build.
To demonstrate how the Builder pattern works, we will turn our hungry eyes to one of the best lunch foods: the glorious pizza.
Here's the thing about pizzas: the only thing that defines a pizza is something edible on top of some kind of cooked dough.
That said, different types of pizzas require other steps to make, but they're still just pizzas. Most of the time, the same ingredients can be used to create many different kinds of pizzas. Let's see how we can use the Builder pattern to build some delicious pizzas.
To start off, we will implement the Director participant. We'll call our Director class AssemblyLine
, make it a class, and it will define in what steps the process of making a pizza are called.
///
/// The Director class
///
public class AssemblyLine
{
// Builder uses a complex series of steps
public void Assemble(PizzaBuilder pizzaBuilder)
{
pizzaBuilder.AddDough();
pizzaBuilder.AddSauce();
pizzaBuilder.AddCheeses();
pizzaBuilder.AddMeats();
pizzaBuilder.AddVeggies();
pizzaBuilder.AddExtras();
}
}
We also need to define the Product participant which is being built by the Builder participant. For our example, the Product is, of course, a Pizza
.
///
/// The Product class
///
public class Pizza {
private string pizzaName;
private Dictionary ingredients =
new Dictionary();
public Pizza(string pizzaName) {
this.pizzaName = pizzaName;
}
// Indexer
public string this[string key] {
get {
return ingredients[key];
}
set {
ingredients[key] = value;
}
}
public void Display() {
Console.WriteLine("\n----------------------------");
Console.WriteLine($"Pizza: {pizzaName}");
Console.WriteLine($" Dough: {ingredients["
dough "]}");
Console.WriteLine($" Sauce: {ingredients["
sauce "]}");
Console.WriteLine($" Meats: {ingredients["
meats "]}");
Console.WriteLine($" Cheeses: {ingredients["
cheeses "]}");
Console.WriteLine($" Veggies: {ingredients["
veggies "]}");
Console.WriteLine($" Extras: {ingredients["
extras "]}");
}
}
Now that we know the definition of the product we are building, let's create the Builder participant. It will be an abstract class named PizzaBuilder
.
///
/// The Builder Abstract class
///
public abstract class PizzaBuilder
{
protected Pizza pizza;
// Get the pizza instance
public Pizza Pizza
{
get { return pizza; }
}
public abstract void AddDough();
public abstract void AddSauce();
public abstract void AddMeats();
public abstract void AddCheeses();
public abstract void AddVeggies();
public abstract void AddExtras();
}
Each subclass of PizzaBuilder will need to implement every abstract method to properly build a pizza.
Next, let's implement a few ConcreteBuilder classes to build
specific pizzas.
///
/// A ConcreteBuilder class
///
class MeatFeastHot : PizzaBuilder
{
public MeatFeastHot()
{
pizza = new Pizza("Meat Feast Hot");
}
public override void AddDough()
{
pizza["dough"] = "Wheat pizza dough";
}
public override void AddSauce()
{
pizza["sauce"] = "Tomato base";
}
public override void AddMeats()
{
pizza["meats"] = "Pepperoni, Ham, Beef, Chicken";
}
public override void AddCheeses()
{
pizza["cheeses"] = "Signature triple cheese blend, mozzarella";
}
public override void AddVeggies()
{
pizza["veggies"] = "";
}
public override void AddExtras()
{
pizza["extras"] = "jalapenos";
}
}
///
/// A ConcreteBuilder class
///
class HotNSpicyVeg : PizzaBuilder
{
public HotNSpicyVeg()
{
pizza = new Pizza("Hot 'N' Spicy Veg");
}
public override void AddDough()
{
pizza["dough"] = "12-grain pizza dough";
}
public override void AddSauce()
{
pizza["sauce"] = "Tomato base";
}
public override void AddMeats()
{
pizza["meats"] = "";
}
public override void AddCheeses()
{
pizza["cheeses"] = "Signature triple cheese blend, mozzarella";
}
public override void AddVeggies()
{
pizza["veggies"] = "Mushrooms, Peppers, Red Onions";
}
public override void AddExtras()
{
pizza["extras"] = "Jalapenos";
}
}
Once we have all our ConcreteBuilder classes ready, we can use them in our Main()
method like so:
static void Main(string[] args)
{
PizzaBuilder builder;
// Create a pizza assembly line
AssemblyLine shop = new AssemblyLine();
// Construct and display pizzas
builder = new MeatFeastHot();
shop.Assemble(builder);
builder.Pizza.Display();
builder = new HotNSpicyVeg();
shop.Assemble(builder);
builder.Pizza.Display();
// Wait for user
Console.ReadKey();
}
The nice thing about this pattern is that we can now reuse the AssemblyLine
class on any PizzaBuilder
we wish, and we have more fine-grained control over how the pizzas are built.
Pros and Cons of Builder Pattern
✔ We can construct objects in a step-by-step fashion, defer the construction steps, or even run steps recursively. | ❌ The overall complexity of the code increases since the pattern requires creating multiple new classes |
✔ We can reuse the same construction code when building various representations of the products. | |
✔ We can isolate complex construction code from the business logic of the product, thus satisfying the Single Responsibility Principle |
Relations with Other Patterns
There are some related patterns to the Builder.
- A lot of designs start as Factory Methods (less complicated but more customizable than subclassing) and evolve toward Abstract Factories, Prototypes, or Builders (more flexible than Factory Methods but more complicated)
- We can use Builders when creating complex Composite trees.
- We can combine the Builder pattern with the Bridge pattern. The director will play the role of the abstraction, while the builders will act as implementations
- Builders are often implemented as Singletons
Builder Pattern Variants
Fluent Builder
Extending the Builder pattern with a fluent API makes it more readable. It also allows us to chain statements for the object configuration. With that, there is no need to specify the builder object with every statement anymore.
public class FluentPizzaBuilder {
private readonly Pizza pizza;
public FluentPizzaBuilder(string pizzaName) {
pizza = new Pizza(pizzaName);
}
public FluentPizzaBuilder WithDough(string dough) {
pizza["dough"] = dough;
return this;
}
public FluentPizzaBuilder WithSauce(string sauce) {
pizza["sauce"] = sauce;
return this;
}
public FluentPizzaBuilder WithMeat(string meat) {
pizza["meats"] = meat;
return this;
}
public FluentPizzaBuilder WithCheese(string cheese) {
pizza["cheeses"] = cheese;
return this;
}
public FluentPizzaBuilder WithVeggie(string veggie) {
pizza["veggies"] = veggie;
return this;
}
public FluentPizzaBuilder WithExtra(string extra) {
pizza["extras"] = extra;
return this;
}
public Pizza Build() {
return pizza;
}
}
We can now use the Fluent Builder to produce a new Pizza.
var pizzaBuilder = new FluentPizzaBuilder("Supreme");
var pizzaSupreme = pizzaBuilder
.WithDough("12-grain pizza dough")
.WithSauce("Tomato base")
.WithMeat("Pepperoni, Seasoned Minced Beef, Spicy Pork Sausage")
.WithVeggie("Mushroom, Mixed Peppers, Red Onions")
.WithCheese("Mozzarella")
.WithExtra("Jalapenos")
.Build();
pizzaSupreme.Display();
Parent-Child Builder
We can use a parent-child relationship with the Builder pattern to
create complex objects. We will first define a parent class tasked to create the complex object and then one or more child classes that create parts of the object.
We need to define some Products which will be built by the Child builders. For this example, we have 3 products, a Side dish, a Salad dish and a Deal that will contain some Sides and a Salad.
public class Side
{
public string Item { get; set; }
public string Dip { get; set; }
public string Size { get; set; }
}
public class Salad
{
public string Base{get;set;}
public string Veggies{get;set;}
public string Meats{get;set;}
public string Cheeses{get;set;}
public string Dressing{get;set;}
}
public class Deal {
public List < Side > Sides {
get;
set;
}
public Salad Salad {
get;
set;
}
public void Display() {
foreach(var side in Sides) {
Console.WriteLine($"Side: {side.Item}");
Console.WriteLine($"Dip: {side.Dip}");
Console.WriteLine($"Size: {side.Size}");
Console.WriteLine();
}
Console.WriteLine("Salad:");
Console.WriteLine($"Base: { Salad.Base}");
Console.WriteLine($"Veggies: {Salad.Veggies}");
Console.WriteLine($"Meats: {Salad.Meats}");
Console.WriteLine($"Cheeses: {Salad.Cheeses}");
Console.WriteLine($"Dressing: {Salad.Dressing}");
}
}
Then we will have to create a Builder for each of our products. Notice that our DealBuilder acts like the parent Builder and calls the SaladBuilder and the SideBuilder that act as the child Builders.
public class DealBuilder
{
private readonly Deal deal = new Deal();
public DealBuilder()
{
deal.Sides = new List();
}
public SaladBuilder AddSalad()
{
return new SaladBuilder(this, deal);
}
public SideBuilder AddSide()
{
return new SideBuilder(this, deal);
}
public Deal Build()
{
return deal;
}
}
public class SaladBuilder
{
private readonly DealBuilder _dealBuilder;
private readonly Deal _deal;
private readonly Salad _salad = new Salad();
public SaladBuilder(DealBuilder parentBuilder, Deal deal)
{
_dealBuilder = parentBuilder;
_deal = deal;
}
public SaladBuilder WithBase(string saladBase)
{
_salad.Base = saladBase;
return this;
}
public SaladBuilder WithVeggies(string veggies)
{
_salad.Veggies = veggies;
return this;
}
public SaladBuilder WithMeats(string meats)
{
_salad.Meats = meats;
return this;
}
public SaladBuilder WithCheeses(string cheeses)
{
_salad.Cheeses = cheeses;
return this;
}
public SaladBuilder WithDressing(string dressing)
{
_salad.Dressing = dressing;
return this;
}
public DealBuilder BuildSalad()
{
_deal.Salad = _salad;
return _dealBuilder;
}
}
public class SideBuilder
{
private DealBuilder dealBuilder;
private Deal deal;
private Side side = new Side();
public SideBuilder(DealBuilder dealBuilder, Deal deal)
{
this.dealBuilder = dealBuilder;
this.deal = deal;
}
public SideBuilder WithItem(string item)
{
side.Item = item;
return this;
}
public SideBuilder WithDip(string dip)
{
side.Dip = dip;
return this;
}
public SideBuilder WithSize(string size)
{
side.Size = size;
return this;
}
public DealBuilder BuildSide()
{
deal.Sides.Add(side);
return dealBuilder;
}
}
We can now use our Builders to create a Deal object:
var deal = new DealBuilder()
.AddSide()
.WithItem("Spicy Loaded Pepperoni Wedges")
.WithDip("Mayo")
.WithSize("Large")
.BuildSide()
.AddSide()
.WithItem("Spicy Cheesy Pepperoni Garlic Bread")
.WithDip("BBQ Sauce")
.WithSize("Large")
.BuildSide()
.AddSalad()
.WithBase("Lettuce")
.WithVeggies("Cherry Tomatoes, Red Cabbage, Carrot")
.WithDressing("Garlic & Herbs")
.BuildSalad()
.Build();
Which will create the following complex object:
Progressive Builder
A progressive builder is a variant of the Builder pattern. It sequentially uses multiple builders to define a fixed sequence of method-chaining calls. The advantage of this implementation is that ensures that the object is built in the correct order.
In the example below we will create a Pizza Deal, which will have some side dishes, a salad, a pizza, some drinks and a dessert.
As always we will first define our Product classes
public class Side {
public string Item {
get;
set;
}
public string Dip {
get;
set;
}
public string Size {
get;
set;
}
public void Display() {
Console.WriteLine("\n--------- Side Dish ---------");
Console.WriteLine($"Item: {Item}");
Console.WriteLine($"Dip: {Dip}");
Console.WriteLine($"Size: {Size}");
}
}
public class Salad {
public string Base {
get;
set;
}
public string Veggies {
get;
set;
}
public string Meats {
get;
set;
}
public string Cheeses {
get;
set;
}
public string Dressing {
get;
set;
}
public void Display() {
Console.WriteLine("\n--------- Salad Dish ---------");
Console.WriteLine($"Base: {Base}");
Console.WriteLine($"Veggies: {Veggies}");
Console.WriteLine($"Meats: {Meats}");
Console.WriteLine($"Cheeses: {Cheeses}");
Console.WriteLine($"Dressing: {Dressing}");
}
}
public class Salad {
public string Base {
get;
set;
}
public string Veggies {
get;
set;
}
public string Meats {
get;
set;
}
public string Cheeses {
get;
set;
}
public string Dressing {
get;
set;
}
public void Display() {
Console.WriteLine("\n--------- Salad Dish ---------");
Console.WriteLine($"Base: {Base}");
Console.WriteLine($"Veggies: {Veggies}");
Console.WriteLine($"Meats: {Meats}");
Console.WriteLine($"Cheeses: {Cheeses}");
Console.WriteLine($"Dressing: {Dressing}");
}
}
public class Drink {
public string Item {
get;
set;
}
public string Size {
get;
set;
}
public void Display() {
Console.WriteLine("\n--------- Drink Dish ---------");
Console.WriteLine($"Item: {Item}");
Console.WriteLine($"Size: {Size}");
}
}
public class Dessert {
public string Item {
get;
set;
}
public string Size {
get;
set;
}
public void Display() {
Console.WriteLine("\n--------- Dessert Dish ---------");
Console.WriteLine($"Item: {Item}");
Console.WriteLine($"Size: {Size}");
}
}
And finally the Deal object
public class MenuDeal {
public List < Side > Sides {
get;
set;
}
public Salad Salad {
get;
set;
}
public Pizza Pizza {
get;
set;
}
public Drink Drink {
get;
set;
}
public Dessert Dessert {
get;
set;
}
public MenuDeal() {
Sides = new List < Side > ();
}
public void Display() {
foreach(var side in Sides)
side.Display();
Salad.Display();
Pizza.Display();
Drink.Display();
Dessert.Display();
}
}
Next, we are going to define our Builder objects. Notice that each Builder can call the next builder in the chain.
public class DessertBuilder {
private MenuDealBuilder menuDealBuilder;
private MenuDeal menuDeal;
private Dessert dessert;
public DessertBuilder(MenuDealBuilder menuDealBuilder, MenuDeal menuDeal) {
this.menuDealBuilder = menuDealBuilder;
this.menuDeal = menuDeal;
dessert = new Dessert();
}
public DessertBuilder WithItem(string item) {
dessert.Item = item;
return this;
}
public DessertBuilder WithSize(string size) {
dessert.Size = size;
return this;
}
public MenuDeal Build() {
menuDeal.Dessert = dessert;
return menuDeal;
}
}
public class SideDishBuilder {
private MenuDealBuilder menuDealBuilder;
private MenuDeal menuDeal;
private Side sideDish;
public SideDishBuilder(MenuDealBuilder menuDealBuilder, MenuDeal menuDeal) {
this.menuDealBuilder = menuDealBuilder;
this.menuDeal = menuDeal;
sideDish = new Side();
}
public SideDishBuilder WithItem(string item) {
sideDish.Item = item;
return this;
}
public SideDishBuilder WithDip(string dip) {
sideDish.Dip = dip;
return this;
}
public SideDishBuilder WithSize(string size) {
sideDish.Size = size;
return this;
}
public SideDishBuilder AddSideDish() {
menuDeal.Sides.Add(sideDish);
return new SideDishBuilder(menuDealBuilder, menuDeal);
}
public SaladBuilder AddSaladDish() {
menuDeal.Sides.Add(sideDish);
return new SaladBuilder(menuDealBuilder, menuDeal);
}
}
using ProgressiveBuilder.Products;
namespace ProgressiveBuilder.Builders {
public class SaladBuilder {
private MenuDealBuilder menuDealBuilder;
private MenuDeal menuDeal;
private Salad salad;
public SaladBuilder(MenuDealBuilder menuDealBuilder, MenuDeal menuDeal) {
this.menuDealBuilder = menuDealBuilder;
this.menuDeal = menuDeal;
salad = new Salad();
}
public SaladBuilder WithBase(string saladBase) {
salad.Base = saladBase;
return this;
}
public SaladBuilder WithVeggies(string veggies) {
salad.Veggies = veggies;
return this;
}
public SaladBuilder WithMeats(string meats) {
salad.Meats = meats;
return this;
}
public SaladBuilder WithCheeses(string cheeses) {
salad.Cheeses = cheeses;
return this;
}
public SaladBuilder WithDressing(string dressing) {
salad.Dressing = dressing;
return this;
}
public PizzaBuilder AddPizzaDish() {
menuDeal.Salad = salad;
return new PizzaBuilder(menuDealBuilder, menuDeal);
}
}
}
public class PizzaBuilder
{
private MenuDealBuilder menuDealBuilder;
private MenuDeal menuDeal;
private Pizza pizza;
public PizzaBuilder(MenuDealBuilder menuDealBuilder, MenuDeal menuDeal)
{
this.menuDealBuilder = menuDealBuilder;
this.menuDeal = menuDeal;
pizza = new Pizza();
}
public PizzaBuilder WithDough(string dough)
{
pizza.Dough = dough;
return this;
}
public PizzaBuilder WithSauce(string sauce)
{
pizza.Sauce = sauce;
return this;
}
public PizzaBuilder WithCheeses(string cheeses)
{
pizza.Cheeses = cheeses;
return this;
}
public PizzaBuilder WithMeats(string meats)
{
pizza.Meats = meats;
return this;
}
public PizzaBuilder WithVeggies(string veggies)
{
pizza.Veggies = veggies;
return this;
}
public PizzaBuilder WithExtras(string extras)
{
pizza.Extras = extras;
return this;
}
public DrinkBuilder AddDrink()
{
menuDeal.Pizza = pizza;
return new DrinkBuilder(menuDealBuilder, menuDeal);
}
}
public class DrinkBuilder {
private MenuDealBuilder dealBuilder;
private MenuDeal menuDeal;
private Drink drink;
public DrinkBuilder(MenuDealBuilder dealBuilder, MenuDeal menuDeal) {
this.dealBuilder = dealBuilder;
this.menuDeal = menuDeal;
drink = new Drink();
}
public DrinkBuilder WithItem(string item) {
drink.Item = item;
return this;
}
public DrinkBuilder WithSize(string size) {
drink.Size = size;
return this;
}
public DessertBuilder AddDesert() {
menuDeal.Drink = drink;
return new DessertBuilder(dealBuilder, menuDeal);
}
}
public class DessertBuilder {
private MenuDealBuilder menuDealBuilder;
private MenuDeal menuDeal;
private Dessert dessert;
public DessertBuilder(MenuDealBuilder menuDealBuilder, MenuDeal menuDeal) {
this.menuDealBuilder = menuDealBuilder;
this.menuDeal = menuDeal;
dessert = new Dessert();
}
public DessertBuilder WithItem(string item) {
dessert.Item = item;
return this;
}
public DessertBuilder WithSize(string size) {
dessert.Size = size;
return this;
}
public MenuDeal Build() {
menuDeal.Dessert = dessert;
return menuDeal;
}
}
The final step is to call the chain of builders to create a deal object:
var menuBuilder = new MenuDealBuilder();
var menu = menuBuilder
.AddSideDish()
.WithItem("Breaded All-White Chicken Breast, Baked in Stone Oven")
.WithDip("Frank's Spicy Buffalo")
.WithSize("Large")
.AddSideDish()
.WithItem("Gluten Free Corn Tortilla Chips")
.WithDip("Guacamole")
.WithSize("Large")
.AddSaladDish()
.WithBase("Fresh Mixed Lettuce")
.WithVeggies("Chopped Onions, Green Bell Peppers, Black Olives, Mushrooms")
.WithMeats("Fajita Chicken, Bacon Bits")
.WithCheeses("Mozzarella")
.WithDressing("Zesty Italian")
.AddPizzaDish()
.WithDough("Sourdough with Cream Cheese Crust")
.WithSauce("Tomato Sauce")
.WithMeats("Beef, Sausage, Pepperoni")
.WithVeggies("Mushrooms, Green Bell Peppers, Onions")
.WithCheeses("Mozzarella")
.AddDrink()
.WithItem("Soda")
.WithSize("Large")
.AddDesert()
.WithItem("Cookie Dough Ice Cream")
.WithSize("Large")
.Build();
menu.Display();
Each Builder will create its own part of the Deal object, and then will call the next builder in the chain. This will ensure that the parts will be created in the correct order.
The above will create this complex object:
Final Thoughts
In this article, we have discussed what is the builder pattern, when to use it and what are the pros and cons of using this design pattern. We then discussed how the Builder pattern relates to other classic design patterns. Finally, we saw some variations of the Builder pattern that either improve the readability of the pattern (FluentBuilder) or address a specific issue.
It's worth noting that the Builder pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (1)
Did you forget to include the
MenuDealBuilder
code?