Builder Design Pattern

What is Builder Pattern?

Builder is a creational design pattern that 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 construction process to create different versions of the object.

Problem statement

To understand the Builder design pattern, let's use the example of making pizzas. The process of making a pizza consists of a series of steps: first, we make the dough, and then we add the base, toppings, and sauce. Finally, we bake the pizza.

Solution approach 1

Creating a complex object like a pizza requires step-by-step initialization of many fields. So, one solution is to define the initialization code inside a constructor with multiple parameters. To implement this, we can define a Pizza class with fields such as dough, base, and toppings, and use the constructor of the Pizza class to initialize these fields.

//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...
}

Drawbacks

  • Technically, the set of overloaded constructors will give an error because the compiler will not be able to distinguish between the second and third constructors due to the same signature. So if the class fields have similar data types, we will have difficulty using constructor overloading for object creation.
  • As the number of combinations of parameters increases, the number of constructors will increase. So this approach is inefficient for complex and heavy classes.

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.

Solution approach 2

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....
//...

Drawbacks

  • To use this approach, consumers have to call the setter methods in the correct order with the appropriate parameters. For example, if a consumer calls setSauce() before setDough(), they won't end up with a pizza. This can be a problem if the consumer doesn't know the proper order of the steps, as it's important to follow the recipe correctly to make a pizza. In simple words, using setter methods requires the consumer to have a good understanding of the process of creating the object.
  • If a consumer requests a specific type of pizza (such as a large Mexican pizza), the consumer code would have to call all the setter methods with the new values. This can be time-consuming and tedious, especially if there are many fields to initialize.

Takeaway

  • 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.
  • 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 separates the construction process from the representation of the object, which will help us to use the same process to create different versions of the object.

An efficient solution using the Builder Pattern

We know that all pizzas follow the same process, but the implementation 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.
  • From another perspective, 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 a Cook interface that declares all the steps involved in making a pizza.

Components and Structure

Components and UML Structure of Builder Design Pattern

Builder (Cook): Interface that declares steps for constructing a product that is common to all of its subclasses or concrete builders.

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. Note: Concrete builders may produce products that don’t follow the common interface.

Director (HeadChef): This class defines the proper order in which all the construction steps should be invoked, so we can create and reuse specific configurations of products. It uses the Builder interface to create an instance of the complex product.

Product (Pizza): The final objects returned by the concrete builders. While in this example all products belong to the same class (Pizza), products can belong to different class hierarchies or interfaces. 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 builder's objects with the Director. The Director uses this builder object to construct the product. Note: The client can do this just once using the constructor parameters of the director. There can be another approach: the client can pass the builder object to the construction method of the director.

Implementation Steps

  1. Declare common construction steps in the builder interface for building all available products.
  2. Create concrete builder classes for each product representation and implement their construction steps.
  3. Add a separate method inside each concrete builder to retrieve the output of the construction. We cannot declare this method inside the builder interface because different builders may construct products that do not have a common interface. Therefore, we do not know the return type for such a method. Note: If working with products from a single hierarchy, we can simply add this method to the base interface.
  4. Create a director class and encapsulate various ways to construct a product using the builder object.
  5. In the client code, create both the builder and director objects, and pass the builder object to the director to start the construction process.

Implementation code Java

Builder Design Pattern Example

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 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 interface Cook, which includes all the steps involved in construction.

public interface Cook {
    public void buildDough();
    public void buildBase();
    public void buildToppings();
    public void buildSauce();
    public void buildBake();
    public void buildCheese();
    public Pizza getPizza();    
}

As mentioned above, 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.
  • 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 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 finalPizza = this.pizza;
        this.pizza = new Pizza();
        return finalPizza;
    }
}

MexicanCook Class

public 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 finalPizza = this.pizza;
        this.pizza = new Pizza();
        return finalPizza;
    }
}

In the getPizza() method, we set the pizza field to a new Pizza object so that the same Cook object 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 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). 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();
}

Key Takeaway

  • Builder pattern allows us to separate two different responsibilities: the order of construction steps (the recipe), and the ingredients used in each step. In other words, we can use the same construction process (provided by the HeadChef) to create different representations of our object (the Pizza).
  • The Cook interface hides the details of the construction process from the client. This means that the client needs to associate a specific Cook through the HeadChef to get their order ready.
  • Adding new types of Cooks is easy because we just need to create a new subclass of Cook and implement it. In contrast, the previous approach required changing the existing client code.
  • We can create additional classes like HeadChef, such as PrimeChef, which may have a different construction process depending on the need. Then, we can use the PrimeChef class with existing Cooks to get new, desirable pizzas!
  • We can also define a new construction process within the HeadChef class itself. In our example, we only considered a single construction process, but there can be many processes with slight differences. For example, a super-fast variant of pizza has some additional steps compared to a regular pizza.
public class HeadChef {
    private Cook cook;
    public HeadChef(Cook cook) {
        this.cook = cook;
    }
    public void makePizza() {
        //...
    }
    
    public void makeSuperFastPizza() {
        //...
    }
}

When to apply Builder Pattern?

We can use Builder when:

  • Using a telescopic constructor is not efficient.
  • The same construction process is used to create different representations of the complex product.
  • The process of creating the product is independent of the parts that make up the object and how they are assembled. In our example, the process of creation is defined by the HeadChef, while the parts that make up the object are implemented by the concrete builders (ItalianCook and MexicanCook).

Consequences

  • Builder object interacts with the director via an abstract interface, and each concrete builder implements that interface to provide a specific representation of the product. So, to add new representations, we need to add a new concrete builder that implements the interface in a specific manner.
  • Builder reduces the responsibility of the client code. In other words, client only needs to associate the correct builder class with the appropriate director class (there may exist more than one director).
  • Builder allows us to use the same construction process to build different representations of the complex product and gives finer control over the construction process. Compared to the factory method or abstract factory method, which creates the objects in a single function call, the builder pattern follows a step-by-step approach directed by the director.
  • Builder makes the application flexible at the cost of increasing complexity and number of classes.

Applications

  • StringBuilder class in Java libraries uses the Builder pattern. It provides methods such as append() and insert() to modify strings more efficiently compared to concatenation using the + operator.
  • OkHttpClient class in the OkHttp library uses the Builder to construct a customized OkHttpClient instance. It provides various methods to set properties such as timeouts, interceptors, SSL configuration, and more.
  • In Android development, AlertDialog.Builder class is used to create alert dialog boxes. It follows the Builder pattern to construct dialog objects with specific configurations, such as setting the title, message, buttons, and other attributes of the dialog.
  • In software testing, Builder is frequently used to create test data objects. For example, Testing frameworks, like JUnit and TestNG, often use Builder to create test configuration objects. These builders enable developers to define and customize test setups, assertions, and other testing configurations.
  • Builder can be used in serialization and deserialization processes. Here builder will read the serialized data and reconstruct the object with all its necessary attributes. For example, Google Gson (a popular Java library for JSON processing) includes the GsonBuilder class. It helps us to configure Gson with various options and provides a way to customize the JSON serialization and deserialization process.
  • RTF converter application of ET++ uses the Builder pattern.

Relations with other patterns

  • Abstract Factory creates families of related objects and returns the product immediately. On the other side, Builder uses some construction steps before returning the product.
  • Builder can be used with Prototype to construct objects by copying an existing object's structure.
  • We can combine Builder with Chain of Responsibility to define a chain of builders, where each builder handles a specific construction step. If a builder cannot handle the step, it passes the responsibility to the next builder in the chain.
  • Builder can be implemented as Singleton.
  • Sometimes, we start the simple object-creation process using the factory method and based on evolving requirements, move towards complex object creation using Abstract Factory, Prototype, or Builder.
  • Builder can be used with the Command to encapsulate construction requests as commands. This will provide more flexibility and customization during the construction process.
  • Builder can use Strategy pattern to vary the construction algorithm dynamically. Different builder strategies can be used interchangeably to construct different variations of the same product.

Thanks to Ankit Nishad for his contribution in creating the first version of this content. If you have any queries or feedback, please write us at contact@enjoyalgorithms.com. Enjoy learning, Enjoy oops!

More from EnjoyAlgorithms

Self-paced Courses and Blogs