The Builder design pattern helps us create complex objects by providing a step-by-step construction process. It separates the construction of the object from its representation, which means we can use the same process to create different versions of the object. This is a creational design pattern that makes it easy to create complex objects.
To help understand the Builder design pattern, let's use the example of making pizzas. A pizza consists of a series of steps: first, we make the dough, then we add the base, toppings, and sauce. Finally, we bake the pizza. The Builder design pattern allows us to follow a clear process to create a complex object, like a pizza, by breaking it down into smaller steps.
We can consider the Pizza class as a representation of a pizza, with fields like dough, base, and toppings. To create a pizza object, we can use the constructor of the Pizza class.
//Pizza class with constructor
public class Pizza {
//...
private String dough;
private String base;
private String toppings;
private String sauce;
private String bake;
private String cheese;
public Pizza (String dough, String base, String toppings,
String sauce, String bake, String cheese)
{
this.dough = dough;
this.base = base;
this.toppings = toppings;
this.sauce = sauce;
this.bake = bake;
this.cheese = cheese;
}
//...
}
Using the Pizza class constructor can help us create a pizza, but what if customers have different preferences? For example, one customer might want extra toppings and mozzarella cheese, while another customer might want no toppings at all. In this case, using a constructor alone may not be sufficient to handle different customization requests. So we need to change our Pizza class and add a set of overloaded constructors like this:
//set of overloaded constructors which are added in Pizza class
public Pizza(String dough, String base, String toppings,
String sauce, String bake, String cheese)
{
//...
}
public Pizza(String dough, String base, String sauce,
String bake, String cheese)
{
//pizza without toppings...
}
public Pizza(String dough, String base, String toppings,
String sauce, String bake)
{
//pizza without cheese...
}
Takeaway: This approach is often termed a telescopic constructor pattern, an anti-pattern. So in place of using this pattern, we should try a better approach.
To address the limitations of the telescopic constructor pattern, we can use setter methods to initialize the fields in a class. This way, we can specify the values for each field individually, rather than using a single constructor with a large number of parameters.
//...
public void setDough (String dough) {
this.dough = dough;
}
public void setBase (String base) {
this.base = base;
}
public void setSauce (String sauce) {
this.sauce = sauce;
}
//and so on....
//...
It's important to consider the order in which the steps are performed, and we don't want the consumer code to be responsible for this. Additionally, there may be many types of pizzas that require each step to be performed differently, which can be difficult to manage in the consumer code.
The Builder pattern is a solution to these problems when creating a complex product. It helps to separate the construction process from the representation of the object, which allows us to use the same process to create different versions of the object.
We know that all pizzas follow the same process, but the implementation of the steps may vary. For example, an Italian pizza has different toppings and cheese from a Mexican pizza, but the steps to make both pizzas are the same. To handle this, we can separate the recipe from the process of creating the pizza.
To do this, we can hire a HeadChef who knows the recipe, and specialized Cooks who can make specific types of pizzas. For example, an ItalianCook knows how to make an Italian pizza, and a MexicanCook knows how to make a Mexican pizza. The HeadChef's role is to provide the recipe to the Cook, who follows the recipe and returns the finished pizza to the HeadChef. This way, the HeadChef can manage the overall process of making the pizza, while the Cooks handle the specific steps for each type of pizza.
Each Cook is expected to follow the company's policies when making a pizza, and they are only allowed to use a common set of steps. However, they must implement these steps according to the requirements of their specific pizza. For example, a MexicanCook can't add a new component to the Pizza class. To solve this issue, we can introduce an interface called Cook that declares all the steps involved in making a pizza.
In terms of classes and interfaces, the HeadChef class defines the steps in the correct order. The Cook interface consists of methods to set the fields required for the Pizza product, such as dough and sauce. Subclasses of Cook, like MexicanCook and ItalianCook, implement the methods provided in the Cook interface. Finally, the completed Pizza product is returned by one of the concrete subclasses of Cook.
Builder (Cook): This is an interface that declares steps for constructing a product that are common to all of its subclasses.
Concrete Builders (ItalianCook, MexicanCook): These classes implement the methods of the Builder interface in different ways to meet the demands of consumers. Different concrete builders provide different implementations, so we can get different versions of the complex product.
Director (HeadChef): This class defines the proper order in which all the construction steps should be invoked. It uses the Builder interface to create an instance of the complex product.
Product (Pizza): The final objects are returned by the concrete builders. While in this example all products belong to the same class (Pizza), it's possible for products to belong to different classes. In that case, the consumer code would call the method of the Concrete builders directly to get the final product.
Client (Consumer): This part of the code associates one of the concrete builders with the director, and then constructs the product using the director class.
First, we create the Product class, Pizza, which has various fields and setter methods for those fields (e.g., setDough(), setBase(), etc). It's important to note that these fields can be objects of other classes as well.
public static class Pizza {
private String dough;
private String base;
private String toppings;
private String sauce;
private String bake;
private String cheese;
public void setDough (String dough) {
this.dough = dough;
}
public void setBase (String base) {
this.base = base;
}
public void setToppings (String toppings) {
this.toppings = toppings;
}
public void setSauce (String sauce) {
this.sauce = sauce;
}
public void setBake (String bake) {
this.bake = bake;
}
public void setCheese (String cheese) {
this.cheese = cheese;
}
public void showPizza () {
System.out.println(dough+", "+base+", "+toppings+", "+sauce+", "+bake+", "+cheese);
}
}
Next, we create the Builder class, Cook, which declares an interface that includes all the steps involved in construction.
public static interface Cook {
public void buildDough();
public void buildBase();
public void buildToppings();
public void buildSauce();
public void buildBake();
public void buildCheese();
public Pizza getPizza();
}
It's important to consider whether the Builder interface should include a method for returning the instance of the Pizza class, or if the concrete subclasses should handle this on their own. This depends on the final representations of the complex product. If all the possible final representations belong to the same Pizza class, it makes sense to include a method in the Builder interface to retrieve the final product.
However, if the final products are very different, it may not be appropriate to represent them with the same base class or interface. In that case, we would need to handle the retrieval of the final product individually in the concrete builder classes.
We can then create subclasses of the Cook interface, such as MexicanCook and ItalianCook, which provide implementations of the construction steps declared in Cook. These concrete builders will apply all the steps and return the final product.
ItalianCook Class
public static class ItalianCook implements Cook {
private Pizza pizza;
public ItalianCook() {
this.pizza = new Pizza();
}
@Override
public void buildDough() {
pizza.setDough("Italian Dough");
}
@Override
public void buildBase() {
pizza.setBase("Italian Base");
}
@Override
public void buildToppings() {
pizza.setToppings("Italian Toppings");
}
@Override
public void buildSauce() {
pizza.setSauce("Italian Sauce");
}
@Override
public void buildBake() {
pizza.setBake("Bake");
}
@Override
public void buildCheese() {
pizza.setCheese("Cheese");
}
@Override
public Pizza getPizza() {
Pizza final_pizza = this.pizza;
this.pizza = new Pizza();
return final_pizza;
}
}
MexicanCook Class
public static class MexicanCook implements Cook {
private Pizza pizza;
public MexicanCook() {
this.pizza = new Pizza();
}
@Override
public void buildDough() {
pizza.setDough("Mexican Dough");
}
@Override
public void buildBase() {
pizza.setBase("Mexican Base");
}
@Override
public void buildToppings() {
pizza.setToppings("Mexican Toppings");
}
@Override
public void buildSauce() {
pizza.setSauce("Mexican Sauce");
}
@Override
public void buildBake() {
pizza.setBake("Bake");
}
@Override
public void buildCheese() {
pizza.setCheese("Cheese");
}
@Override
public Pizza getPizza() {
Pizza final_pizza = this.pizza;
this.pizza = new Pizza();
return final_pizza;
}
}
In the getPizza() method, we set the pizza field to a new Pizza object so that the same Cook object (or a concrete subclass of Cook) can be used to make more pizzas in the future.
Then, we declare the Director class, HeadChef, which is responsible for defining the order in which the steps are executed. There can be multiple Directors, or a single Director may have multiple construction processes.
public static class HeadChef {
private Cook cook;
public HeadChef (Cook cook) {
this.cook = cook;
}
public void makePizza() {
cook.buildDough();
cook.buildBase();
cook.buildToppings();
cook.buildSauce();
cook.buildBake();
cook.buildCheese();
}
}
Finally, we create the Consumer code, which creates a concrete builder object and passes it to the Director (HeadChef). The Director then uses this builder object to apply the construction process, and the complex product is retrieved either through the Director or directly from the builder.
It's also possible to retrieve the final product from the builder because the Client typically configures the Director with the appropriate concrete builder. This means the Client knows which concrete builder will produce the desired product.
public static void main (String[] args){
Cook cook = new ItalianCook();
HeadChef headchef = new HeadChef (cook);
headchef.MakePizza();
Pizza pizza = cook.GetPizza();
pizza.ShowPizza();
cook = new MexicanCook();
headchef = new HeadChef (cook);
headchef.MakePizza();
pizza = cook.GetPizza();
pizza.ShowPizza();
}
Note: We can create additional classes like HeadChef, such as PrimeChef, which may have a different construction process depending on the needs of the company. Alternatively, we can define a new construction process within the HeadChef class itself. Then, we can use the PrimeChef class with existing Cooks to get new, desirable pizzas! In our example, we only considered a single construction process, but there can be many processes with slight differences. For example, perhaps a super-fast variant of pizza has some additional steps compared to a regular pizza.
We can use the Builder pattern when:
In our example of making pizzas, the process of creation is defined by the HeadChef class, while the parts that make up the object are implemented in the concrete builders (ItalianCook and MexicanCook). Notice that the Builder class (Cook) hides the internal representation of the Pizza product, meaning it hides the classes that define the various parts of the pizza and how they are assembled to create the final product. This means the process defined by the HeadChef class is independent of the parts of the pizza product.
It is also important to be able to easily add new representations and remove existing representations of the complex product. The Builder pattern allows for this flexibility.
Some popular uses of the builder pattern can be found
We saw some possible but inefficient approaches such as construction involving telescopic constructor and construction using setter methods via consumer code. Then, we saw the builder pattern and explored how this pattern solves the problems involved in previous approaches. For e.g, highly responsible consumer code, usage of the same process in the correct order, etc. Finally, we saw some of its applicability and its effect on the entire creational process.
Enjoy learning, Enjoy OOPS!