What is the Builder Creational Design Pattern and why do we need it?
In real life, a builder constructs complex things and does this in small steps, piece by piece. In software development, the Builder Design Pattern allows us to handle the creation of different types and representations of a rather complex object by using the same creation code. To understand better what is all about, let’s have a look at a real-world inspired sample. The C# 11 source code is available on GitHub.
How and why the Builder Design Pattern is used: an example of implementation
We are going to extend the furniture shop application introduced in the post related to Abstract Factory. This time, to keep things simple, the focus will be on just one product, the coffee table, but the same approach can be used to build the other sample products.
The furniture shop application is capable of producing furniture sets in different variants, like Modern, Classic, and Retro. Different products like armchairs, sofas, and coffee tables are available for each variant. Each furniture piece or set is manufactured upon request from a Client.
In the sample for the Abstract Factory, the MakeCoffeeTable() method implementation for each variant is naive, taking some arguments to customize the product’s size and color, this approach being far from reality. Each variant should have a set of predefined dimensional values and/or colors, and the code for the Builder was changed to implement this approach. Additionally, there are specific details about the table plate, handlers, legs (binding components, screws, and rollers), and maybe more.
It’s obvious that things are getting complicated, so why not delegate the job to a specialized component? This is the point at which the builder (or a carpenter if you want) enters the stage.
The standard UML diagram for the Builder Design Pattern looks like this:
The Director class’s responsibility is to create an object using the Builder interface. It specifies the order of building steps, while a ConcreteBuilder provides the implementation for those steps. It’s not required for our application to have a Director class. The building steps may be called directly from the Client code in a specific order. However, the Director class might be a suitable location to place various construction routines so we can reuse them across our application. Moreover, the Director class completely hides from the Client code the details of Product construction. The Client only needs to associate a Builder with a Director, have the Director start construction, and have the Builder deliver the finished product.
Builder defines an abstract interface for creating Product object parts (all the required steps).
ConcreteBuilder constructs and assembles parts of the product by implementing the Builder interface which also contains a method for retrieving the product (GetResult()). Of course, we can have more than one ConcreteBuilder, each responsible for creating a specific product.
The Product represents the complex object under construction and includes classes that define the constituent parts.
In our particular case with the furniture shop, we have a deviation from the standard definition. The reason for this custom approach is to emphasize the fact that we should adapt the design patterns to our particular needs, instead of forcing our implementation to fit into a pattern blueprint. Moreover, we want to improve our existing implementation based on Abstract Factory and that’s another reason for this deviation.
In our particular example, the RetroFurnitureFactory acts as the Director; its implementation of the MakeCoffeeTable() – the equivalent of the Costruct() – instantiates the CoffeeTableBuilder class. In the standard definition of the pattern, the Director is supposed to receive the instance of the ConcreteBuilder via Dependency Injection, but in our implementation, we have a particular factory (responsible for a product variant and a method for a CoffeeTable), so it is a good idea to use the above-mentioned approach. Moreover, the sample implementation is relying on .Net Generics, to provide the required abstraction for the CoffeeTable building.
CoffeeTableBuilder provides the implementation of the “steps”: BuildTableLegs(), BuildTablePlate(), and BuildTableHandles(). GetResult() method is responsible to provide an instance of the RetroCoffeeTable object (in this particular case) to the Client.
For other product variants like Modern and Classic, we have a similar implementation in the sample code.
In this way, we managed to simplify the code from the previous implementation and to provide support for a flexible construction of the “products”.
The essence of it: advantages and disadvantages of the Builder Design Pattern
We should use the Builder Design Pattern when we want:
- To get rid of the multiple constructors of a class, where one differs from the others just by the number of parameters.
- As long as this pattern allows us to build objects step by step, using only those steps that we need, we can simplify our code by reducing the number of these constructors, or, much better, by getting rid of them entirely.
- Our code to create different representations of some Product (different variants of coffee tables, armchairs, and sofas).
- To create composite object hierarchies or other complex objects.
There are some disadvantages, like:
- Although there are at least twice as many lines of code, the effort pays off in terms of design flexibility and code readability.
- Requires developing a specific ConcreteBuilder for every different Product type.
While carrying out construction steps, a builder should not expose the unfinished product. This prevents the client code from fetching an incomplete result.
That was for the Builder Design Pattern. The C# source code for the presented sample can be found on GitHub.