As stands from the name, Builder design pattern helps to instantiate objects where instantiation logic is somewhat complex, involves multiple steps or options before an object is ready to be used. Builder design pattern helps to reduce complexity by proving clean object instantiation interface guiding users through all available options. Additionally, Builder helps to improve code readability when Fluent API is used.
Design
Objects that implement Builder design pattern usually go through three phases of the life cycle.
- Configure. Builder class has variety of method that help to setup and configure its object. During this phase client code uses the builder object’s methods to achieve desired configuration.
- Build. Builder class implements Build() method that creates and returns a target object. During this phase client code calls the Build() method and receives a reference to fully constructed target object.
- Use. Client code uses fully constructed target object. Builder object may no longer be needed and can be discarded. This phase is outside of Builder design pattern scope.
The definition might sound little abstract, so let’s get to examples. We will use domain model of a coffee shop and particularly we will model coffee ordering process, where a customer selects coffee cup size, type of the drink and has multiple option, for example to add milk, sugar, maple syrup, vanilla and etc. To keep the example simple, we will focus on the Builder part of the implementation.
To model the coffee ordering process, we will implement two classes Coffee and CoffeBuilder. CoffeeBuilder is Builder design pattern implementation. Coffee is a target class that represents an object that CoffeeBuilder helps to create.
In order to create Coffee object a user must provide a cup size and a drink type before other options can be configured. To enforce this rule CoffeeBuilder constructor will take two arguments CupSize and DrinkType. The approach is not necessary dictated by Builder design pattern. We ask to provide CupSize and DrinkType as CoffeeBuilder constructor arguments to reveal CoffeeBuilder API intentions and communicate to users that CupSize and DrinkType are required.
In order to allow Coffee customization CoffeeBuilder provides multiple Add*() methods that allow uses to customize a drink. So far, the Add*() methods and CoffeeBuilder constructor described above belong to the first phase of Builder design pattern.
Now after we have completed customization it is time to create a Coffee object. For that purpose CoffeeBuilder implements Build() method that produces new instance of Coffee class. The method belongs to the second phase of the life cycle..
We need to make a remark that single instance of CoffeeBuilder may produce multipole copies of Coffee object, or it may be restricted to create only a single instance of Coffee class. Either way is fine as Builder design pattern does not limit our options. You are free to implement either approach depends on what better addresses your needs.
Implementation
Coffee class implementation is straightforward, the main purpose of it is to describe an ordered coffee drink. Code should speak for itself.
public class Coffee { public DrinkType DrinkType { get; set; } public CupSize CupSize { get; set; } public bool IsDairyFree { get; set; } public bool HasSugar { get; set; } public ICollection<Flavor> Flavors { get; } = new List<Flavor>(); public double TotalCalories { get; set; } }
CoffeeBuilder class obviously has more to it, therefore to keep it simple we will look at the CoffeeBuilder code one life cycle phase at a time.
Configuration Phase
CoffeeBuilder implements multiple methods that allow to configure future instance of Coffee. The methods can be invoked in any order and as many times as needed. Usually, method names of the configuration phase start with “Add”, “Use”, “Apply” and alike verbs. In our case the methods are AddSugar, AddMilk, AddMapleSyrup and AddVanilla.
public class CoffeeBuilder { private readonly CupSize _cupSize; private readonly DrinkType _drinkType; private double _spoonsOfSugar ; private double _milkOz; private double _mapleSyrupOz; private double _vanillaOz; public CoffeeBuilder(CupSize cupSize, DrinkType drinkType) { _cupSize = cupSize; _drinkType = drinkType; } public void AddSugar(double spoons) { _spoonsOfSugar += spoons; } public void AddMilk(double oz) { _milkOz += oz; } public void AddMapleSyrup(double oz) { _mapleSyrupOz += oz; } public void AddVanilla(double oz) { _vanillaOz += oz; } ... }
Build Phase
Usually this phase is implemented by a single method Build() that takes in consideration all configurations and creates an instance of a target class, in our case it’s an instance of Coffee.
public class CoffeeBuilder { .... public Coffee Build() { Coffee coffee = new() { CupSize = _cupSize, DrinkType = _drinkType, HasSugar = _spoonsOfSugar > 0 || _mapleSyrupOz > 0, IsDairyFree = _milkOz == 0 && _drinkType != DrinkType.Latte, TotalCalories = CalculateTotalCalories(), }; if (_vanillaOz > 0) { coffee.Flavors.Add(Flavor.Vanilla); } return coffee; } private double CalculateTotalCalories() { // Calories calculation formula. return 0; } }
Use Phase
Use phase of the Builder pattern is outside of its implementation scope. After Build() method is invoked a fully constructed instance of a target class is returned and client code may start using it.
// Configuration Phase var builder = new CoffeeBuilder(CupSize.Medium, DrinkType.Latte); builder.AddMilk(2); builder.AddSugar(1); builder.AddVanilla(3); builder.AddSugar(-1); // ← Customer changed mind, removing sugar builder.AddMapleSyrup(2); // ← Adding Maple Syrup instead // Build Phase var coffee = builder.Build(); // Use Phase Console.Write($"Got my {coffee.DrinkType} coffee!");
Fluent Interface
There is one important and quite simple enhancement we can apply to the Builder design pattern implementation. It is known as Fluent Interface. Fluent interface allows to chain methods to create highly expressive and very well readable code. Let’s rewrite CoffeeBuider “Add” methods with Fluent Interface.
public class CoffeeBuilderFluent { ... public CoffeeBuilderFluent AddSugar(double spoons) { _spoonsOfSugar += spoons; // ↓ Notice that we return reference to "this" to allow methods chaining. return this; } public CoffeeBuilderFluent AddMilk(double oz) { _milkOz += oz; // ↓ Returning reference to "this" as well to allow methods chaining. return this; } public CoffeeBuilderFluent AddMapleSyrup(double oz) { _mapleSyrupOz += oz; // ↓ Same here. return this; } public CoffeeBuilderFluent AddVanilla(double oz) { _vanillaOz += oz; // ↓ And here. return this; } public Coffee Build() { // Same Implementation as above } ... }
And this is how CoffeeBuilder can be used now to configure future Coffee instance.
var coffee = new CoffeeBuilderFluent(CupSize.Medium, DrinkType.Latte) .AddMilk(2) // ← Configuration Phase .AddSugar(1) .AddVanilla(3) .AddSugar(-1) .AddMapleSyrup(2) .Build(); // ← Build Phase // Use Phase Console.Write($"Got my {coffee.DrinkType} coffee!");
If you are familiar with ASP.NET, then you may find Builder pattern is heavily used to configure and setup request processing pipelines. Below is an example from default scaffolded web application using Builder design pattern to configure and build the whole application.
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build();
Builder VS Factory design pattern
Both Builder and Factory design patterns help with objects instantiation, however how they are different and when to use which?
Builder helps to instantiate objects with non-trivial process of creation and configuration. It allows new object to be configured before an instance is ready to be used and has distinct lifecycle phases described above. In general, it helps to address “how” part of object creation problem; e.g. how should I create an object.
Factory also creates objects, but it does not provide elaborate configuration options for new object; all possible options are passed as parameters to factory “Create” methods. Factory helps to instantiate hierarchies of objects and provides unified interface to achieve so. It mostly helps to address “what” part of object creation problem; e.g. what object type should I create.
As always, happy coding!
All code examples can be found on GitHub PavelHudau/BlogCodeExamples.
More design pattern article can be found here.