Builder pattern is one of the 23 Gang of Four (GoF) design patterns aimed at solving frequently occuring design problems during an application development. It is categorized as a Creational Pattern which solves the issue of creating a complex object which should result in different representations (variations) based on the requirements.
The Complex Object Problem
Imagine a bike building application that specializes in developing bikes of various categories like Sports, Cruisers or Regular bikes and of different shapes and specifications. The Bike to be developed is a complex entity with a large constructor that sets all of its specs at one go as below:
public class Bike
{
public int EngineCapacity { get; }
public int Seats { get; }
public int FuelCapacity { get; }
public string ModelName { get; }
public string SeriesName { get; }
public string BikeCategory { get; }
public Bike(
string modelName,
string seriesName,
string category,
int fuelCapacity,
int engineCapacity,
int seats)
{
this.ModelName = modelName;
this.SeriesName = seriesName;
this.Seats = seats;
this.BikeCategory = category;
this.EngineCapacity = engineCapacity;
this.FuelCapacity = fuelCapacity;
}
}
To make a bike, one must fill in all of these specs into the constructor. Also, when this bike needs to be transformed to fit into various types offered by the application (Sports, Cruiser or Regular) not all these values might need to be fed to the entity. This creates a lack of flexibility and tight coupling with the object creation to its representation.
Builder pattern solves this by separating the instantiation logic from its representation, so that the client where the Bike needs to be instantiated need not call a new keyword on the Bike and instead make use of a "Builder" that gets the job done.
What is a Builder?
A Builder is a class which is responsible for creating a particular variation of an entity. A client requests a Builder for an instance of given specs and the builder encapsulates the instantation for that entity. A builder also offers customization of the entity; the client can be able to set only those specs which is needed while ignoring the other. The builder assembles all the specs of the entity based on the client requirements and returns a complex object that represents the particular representation of the entity that the client requires.
Sometimes a Client doesn’t invoke a Builder directly; it instead makes use of a Director that internally invokes a builder to assemble a particular representation of the entity and offers the Client an abstraction from the builder. Although using a Director is optional and upto the application design, it creates a sort of simplicity on the Client part while abstracting the Builder invocation from the client.
Example – Implementing a Builder
For the above Bike entity, let’s say we would want to create an object representing a SportsBike, a Cruiser and a regular Moped. For this instead of creating three instances by passing the values to the lenghty constructor, we can create a BikeBuilder class that looks after the building logic, which is as below:
public interface IBikeBuilder
{
void SetEngineCapacity(int capacity);
void SetSeats(int seats);
void SetFuelCapacity(int fuelCapacity);
void SetModelName(string modelName);
void SetSeriesName(string seriesName);
void SetBikeCategory(string bikeCapacity);
Bike Make();
}
public class BikeBuilder : IBikeBuilder
{
private string _bikeCategory;
private int _engineCapacity;
private int _fuelCapacity;
private string _modelName;
private int _seats;
private string _seriesName;
// build a Bike object based on
// the customization and specs
public Bike Make()
{
return new Bike(
_modelName,
_seriesName,
_bikeCategory,
_fuelCapacity,
_engineCapacity,
_seats);
}
public void SetBikeCategory(string bikeCapacity)
{
_bikeCategory = bikeCapacity;
}
public void SetEngineCapacity(int capacity)
{
_engineCapacity = capacity;
}
public void SetFuelCapacity(int fuelCapacity)
{
_fuelCapacity = fuelCapacity;
}
public void SetModelName(string modelName)
{
_modelName = modelName;
}
public void SetSeats(int seats)
{
_seats = seats;
}
public void SetSeriesName(string seriesName)
{
_seriesName = seriesName;
}
}
The BikeBuilder completely encapsulates the instantation logic for the Bike object and also exposes setter methods for each specification of the Bike instance. To create a Bike, we can call what all the specs we would want on our Bike and the Builder assembles all these specs onto the resulting Bike object when the Make() method is called.
To simplify things even further, we add a Director who is responsible for invoking this builder for a tailor made experience. The Director looks like below:
public class BikeBuildDirector : IBikeBuildDirector
{
private readonly IBikeBuilder _builder;
public BikeBuildDirector(IBikeBuilder builder)
{
_builder = builder;
}
public Bike MakeR1()
{
_builder.SetBikeCategory(BikeTypes.Sports);
_builder.SetEngineCapacity(1000);
_builder.SetFuelCapacity(20);
_builder.SetModelName("Yamaha R1");
_builder.SetSeats(1);
_builder.SetSeriesName("R Series");
return _builder.Make();
}
public Bike MakeChetak()
{
_builder.SetBikeCategory(BikeTypes.Regular);
_builder.SetEngineCapacity(80);
_builder.SetFuelCapacity(15);
_builder.SetModelName("Chetak");
_builder.SetSeriesName("Chetak");
_builder.SetSeats(2);
return _builder.Make();
}
public Bike MakeAvenger()
{
_builder.SetBikeCategory(BikeTypes.Cruiser);
_builder.SetEngineCapacity(220);
_builder.SetFuelCapacity(15);
_builder.SetModelName("Avenger 220");
_builder.SetSeriesName("Avenger");
_builder.SetSeats(2);
return _builder.Make();
}
}
Finally the Client creates an instance of the Director and supplies an instance of the Builder it wishes to use. The Director calls the Make() method on the supplied builder which returns the customized and assembled Bike.
static void Main(string[] args)
{
IBikeBuilder builder = new BikeBuilder();
IBikeBuildDirector director = new BikeBuildDirector(builder);
//build an R1
Bike r1 = director.MakeR1();
Describe(r1);
//build an Avenger
Bike avenger = director.MakeAvenger();
Describe(avenger);
//build a chetak
Bike chetak = director.MakeChetak();
Describe(chetak);
}
The output would be something like this:
EngineCapacity:1000
Seats:1
FuelCapacity:20
ModelName:Yamaha R1
SeriesName:R Series
BikeCategory:SPORTS
EngineCapacity:220
Seats:2
FuelCapacity:15
ModelName:Avenger 220
SeriesName:Avenger
BikeCategory:CRUISER
EngineCapacity:80
Seats:2
FuelCapacity:15
ModelName:Chetak
SeriesName:Chetak
BikeCategory:REGULAR
Builder vs Abstract Factory – Similarities
Builder pattern has a few similarities with the Abstract Factory, which is also a creational pattern aimed at instantiating a specific entity from a family of factory types.
Generally, when we need to create different representations of a single entity based on the scenarios, we would first start by creating a Factory that returns a specific object based on the input criteria and as the application grows in size, we move from a simple Factory into a Builder which offers an assembled instance or an Abstract Factory that returns an instance of a fixed spec.
- In Abstract Factory we have multiple concrete types of a single abstract type and an object returned from an Abstract Factory can be one among many concrete implementations of that abstract type.
- Whereas in a Builder, all the times the application works on only a single complex type but the specs of that type varies with the requirements.
- Since Builder offers a series of assembling methods on top of the object to be created, it is slower than an Abstract Factory.
Advantages of using a Builder
Like mentioned above, using a Builder gives us certain advantages:
- Varying the entity’s representation based on the requirements
- Encapsulates the entity object creation logic
- Provides customization on how the entity object is created by means of a step-by-step assembly
Things to keep in Mind
- A varying builder implies building a new Builder implementation each time a product varies. In our case, we handled it with a Director that takes care of the variation on top of a Generic BikeBuilder approach; it might not be possible all the times.
- The complexity of the code increases since we’re introducing several levels (Builder, Director) between an Entity and a Client.
The complete example is available in the repository: https://github.com/referbruv/builder-pattern-example