Design Patterns: The Builder Pattern
The builder pattern is a very popular pattern and appears regularly in object oriented designs. The main purpose for the builder pattern is to simplify the creation and the management of complex domain objects. In large object oriented designs, class definitions often grow as the number of properties and methods increases. It is often a challenge to initialize these properties correctly as instances of classes are created. Instead of relying on the layer consuming these objects to properly create these instances, the builder pattern abstract this logic into a separate builder class.
Below is an example of a Vehicle class that represents an automobile of some sort. It has a number of properties such as the Engine Size, the Transmission Type of the vehicle and few other properties. In more advanced and complex design, the number of properties can be much more. But in this example the number of properties is kept small for simplicity. There are also two enum classes that are declared to support the declaration of the Vehicle class
Vehicle.cs
public class Vehicle
{
public float EngineSize { get; set; }
public TransmissionType TransmissionType { get; set; }
public AxelType AxelType { get; set; }
public bool IsConvertable { get; set; }
public string Options { get; set; }
}public enum TransmissionType
{
Manual,
Automatic
}public enum AxelType
{
FrontWheel,
RearWheel,
FourWheel
}
In the vehicle example above, the creation of a couple of instances of a vehicle can be cumbersome and it can involve several steps. This is done by setting the properties of the vehicle one at a time. This approach is error prone as the client creating an instance of the class can fail to set some of the properties. Moreover, the initialization order can be critical as some properties may rely on others. Here is a sample of how creating a vehicle with “standard” options and another with “luxury” options may look like:
Program.cs and Main
public class Program
{
public static void Main(string[] args)
{
Vehicle standardVehicle = new Vehicle(); standardVehicle.EngineSize = 1.8;
standardVehicle.TransmissionType = TransmissionType.Manual;
standardVehicle.AxelType = AxelType.FrontWheel;
standardVehicle.IsConvertable = false;
standardVehicle.Options = "A/C, Radio"; Vehicle luxuryVehicle = new Vehicle(); luxuryVehicle.EngineSize = 6.0;
luxuryVehicle.TransmissionType = TransmissionType.Automatic;
luxuryVehicle.AxelType = AxelType.FourWheel;
luxuryVehicle.IsConvertable = true;
luxuryVehicle.Options = "AC, GPS, XM Radio, Leather Interior, Power Windows";
}
}
Creating each instance required providing a value for each property without enforcing a specific order if an order existed. Also, this approach will not scale if several instances of the same vehicle is required as the same logic will be repeated for each instance.
Using a Constructor
One approach maybe to create a constructor that accepts all required parameters to initialize all the parameters of a vehicle. This approach certainly reduces the number of the lines of code required and it does encapsulate the initialization logic into a single class. However, as the number of parameters increases, supplying the values of the parameters becomes more difficult. For example, if a standard vehicle is always expected to have an engine size of 1.8 liters, the constructor cannot prevent the client from setting the engine size to some other arbitrary value that is not applicable. However, depending of the situation, this approach might work. Below is an example of the vehicle class with a constructor. Note how all the properties of the vehicle are converted into private values.
Vehicle.cs
public class Vehicle
{
private readonly double _engineSize;
private readonly TransmissionType _transmissionType;
private readonly AxelType _axelType;
private readonly bool _isConvertable;
private readonly string _options; public Vehicle(double engineSize, TransmissionType transmissionType, AxelType axelType, bool isConvertable, string options)
{
_engineSize = engineSize;
_transmissionType = transmissionType;
_axelType = axelType;
_isConvertable = isConvertable;
_options = options;
}
}
Program.cs and Main
public class Program
{
public static void Main(string[] args)
{
Vehicle standardVehicle = new Vehicle(1.8, TransmissionType.Manual, AxelType.FrontWheel, false, "A/C Radio"); Vehicle luxuryVehicle = new Vehicle(6.0, TransmissionType.Automatic, AxelType.FourWheel, true,
"AC, GPS, XM Radio, Leather Interior, Power Windows");
}
}
Using a Builder Service
In order to centralize the vehicle creation logic into a central location and to automate the creation process, the builder pattern suggest creating a ‘Builder’ class for each brand or type of a vehicle. The ‘BuildVehicle’ method in the builder class is responsible for creating a new instance of a vehicle and knows how to initialize the properties with the correct values every time a new instance is desired. The builder class will contain a private instance of the vehicle and it will contain a ‘GetVehicle’ method that returns the initialized instance. The Vehicle class in this case in kept as simple as a POCO class that strictly contains the properties of a vehicle.
Vehicle.cs
public class Vehicle
{
public double EngineSize { get; set; }
public TransmissionType TransmissionType { get; set; }
public AxelType AxelType { get; set; }
public bool IsConvertable { get; set; }
public string Options { get; set; }
}
StandardVehicleBuilder.cs
public class StandardVehicleBuilder
{
private Vehicle _vehicle; public Vehicle GetVehicle()
{
return _vehicle;
} public void BuildVehicle()
{
_vehicle = new Vehicle(); _vehicle.EngineSize = 1.8;
_vehicle.TransmissionType = TransmissionType.Manual;
_vehicle.AxelType = AxelType.FrontWheel;
_vehicle.IsConvertable = false;
_vehicle.Options = "A/C, Radio";
}
}
LuxuryVehicleBuilder.cs
public class LuxuryVehicleBuilder
{
private Vehicle _vehicle; public Vehicle GetVehicle()
{
return _vehicle;
} public void BuildVehicle()
{
_vehicle = new Vehicle();
_vehicle.EngineSize = 6.0;
_vehicle.TransmissionType = TransmissionType.Automatic;
_vehicle.AxelType = AxelType.FourWheel;
_vehicle.IsConvertable = true;
_vehicle.Options = "AC, GPS, XM Radio, Leather Interior, Power Windows";
}
}
Program.cs and Main
public class Program
{
public static void Main(string[] args)
{
StandardVehicleBuilder standardVehicleBuilder = new StandardVehicleBuilder();
standardVehicleBuilder.BuildVehicle(); Vehicle standardVehicle = standardVehicleBuilder.GetVehicle(); LuxuryVehicleBuilder luxuryVehicleBuilder = new LuxuryVehicleBuilder();
luxuryVehicleBuilder.BuildVehicle(); Vehicle luxuryVehicle = luxuryVehicleBuilder.GetVehicle();
}
}
Introducing the Abstract Builder Class
The introduction of the two builder classes above, the StandardVehicleBuild and the LuxuryVehicleBuilder simplifies the implementation and eliminates any potential errors in creating new instances. However, there is plenty of code that is duplicated among the various builder classes and it does not guarantee that builders implement the appropriate ‘build’ method. In order to enforce the integrity of a builder class through polymorphism, an abstract VehicleBuilder class is introduced to contain all code common to the various builders.
The abstract VehicleBuilder class will house a protected instance of the vehicle class, and it will contain an implementation of the common GetVehicle() and the BuildVehicle() methods. More improtantly, it will contain an abstract declaration of several new methods that are responsible for setting the individual properties of the vehicle class. These methods include SetEngineSize(), SetTransmissionType(), SetAxelType(), SetIsConvertable() and SetOptions().
VehicleBuilder.cs
public abstract class VehicleBuilder
{
protected Vehicle Vehicle; public Vehicle GetVehicle()
{
return Vehicle;
} public void BuildVehicle()
{
Vehicle = new Vehicle(); SetEngineSize();
SetTransmissionType();
SetAxelType();
SetIsConvertable();
SetOptions();
} public abstract void SetEngineSize();
public abstract void SetTransmissionType();
public abstract void SetAxelType();
public abstract void SetIsConvertable();
public abstract void SetOptions();
}
Once the abstract parent VehicleBuilder class is declared, it is implemented by each concrete builder, in this case the StandardVehicleBuilder and LuxuryVehicleBuilder. This results in concrete builder classes that are strictly data classes. Each implementation does nothing more than setting the values of each of the properties to the correct value. E.g. the StandardVehicleBuilder always sets the Engine Size to 1.8 liters.
StandardVehicleBuilder.cs
public class StandardVehicleBuilder : VehicleBuilder
{
public override void SetEngineSize()
{
Vehicle.EngineSize = 1.8;
} public override void SetTransmissionType()
{
Vehicle.TransmissionType = TransmissionType.Manual;
} public override void SetAxelType()
{
Vehicle.AxelType = AxelType.FrontWheel;
} public override void SetIsConvertable()
{
Vehicle.IsConvertable = false;
} public override void SetOptions()
{
Vehicle.Options = "A/C, Radio";
}
}
LuxuryVehicleBuilder.cs
public class LuxuryVehicleBuilder : VehicleBuilder
{
public override void SetEngineSize()
{
Vehicle.EngineSize = 6.0;
} public override void SetTransmissionType()
{
Vehicle.TransmissionType = TransmissionType.Automatic;
} public override void SetAxelType()
{
Vehicle.AxelType = AxelType.FourWheel;
} public override void SetIsConvertable()
{
Vehicle.IsConvertable = true;
} public override void SetOptions()
{
Vehicle.Options = "AC, GPS, XM Radio, Leather Interior, Power Windows";
}
}
Program.cs and Main
public static void Main(string[] args)
{
VehicleBuilder standardVehicleBuilder = new StandardVehicleBuilder();
standardVehicleBuilder.BuildVehicle(); Vehicle standardVehicle = standardVehicleBuilder.GetVehicle(); VehicleBuilder luxuryVehicleBuilder = new LuxuryVehicleBuilder();
luxuryVehicleBuilder.BuildVehicle(); Vehicle luxuryVehicle = luxuryVehicleBuilder.GetVehicle();
}
Introducing the Build Manager
The abstract VehicleBuilder class provides the proper structure of a concrete builder and enforces the definition of all the required methods. However, the order in which the build steps is enforced so far rigid and not easily overridden. The design can be enhanced further by abstracting out the build method and relying on an implementation of a ‘Build Manager’ to invoke the build methods in any desired order. The final design would look like the following
VehicleBuildManager.cs
public class VehicleBuildManager
{
public VehicleBuilder VehicleBuilder { get; set; } public VehicleBuildManager(VehicleBuilder vehicleBuilder)
{
VehicleBuilder = vehicleBuilder;
} public void BuildVehicle()
{
VehicleBuilder.BuildVehicle();
VehicleBuilder.SetEngineSize();
VehicleBuilder.SetTransmissionType();
VehicleBuilder.SetAxelType();
VehicleBuilder.SetIsConvertable();
VehicleBuilder.SetOptions();
}
}
VehicleBuilder.cs
public abstract class VehicleBuilder
{
protected Vehicle Vehicle; public Vehicle GetVehicle()
{
return Vehicle;
} public void BuildVehicle()
{
Vehicle = new Vehicle();
} public abstract void SetEngineSize();
public abstract void SetTransmissionType();
public abstract void SetAxelType();
public abstract void SetIsConvertable();
public abstract void SetOptions();
}
Program.cs and Main
public class Program
{
public static void Main(string[] args)
{
VehicleBuilder standardVehicleBuilder = new StandardVehicleBuilder();
VehicleBuildManager vehicleBuildManager = new VehicleBuildManager(standardVehicleBuilder);
vehicleBuildManager.BuildVehicle();
Vehicle standardVehicle = standardVehicleBuilder.GetVehicle(); VehicleBuilder luxuryVehicleBuilder = new LuxuryVehicleBuilder();
vehicleBuildManager = new VehicleBuildManager(luxuryVehicleBuilder);
vehicleBuildManager.BuildVehicle(); Vehicle luxuryVehicle = luxuryVehicleBuilder.GetVehicle();
}
}
Variations
There are many variations of this pattern. There are also several other patterns that are mistaken for the builder pattern. One of the most common variation requires concrete builders to implement a common interface and then providing a fluent style API for setting each property one at a time.
Final Thoughts
As the completed builder pattern introducing a set of concrete builders and a builder manager to create instances of vehicles, it is worth considering using an IOC container to assist in the create of these instances as more dependencies are introduced in the design.