2.4. Builder

Maheshmaddi
4 min readApr 9, 2023

--

The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation. It enables the same construction process to create different representations of the object. This pattern is particularly useful when dealing with complex objects that require a multi-step initialization process or when the object’s construction logic should be decoupled from its representation.

The Builder pattern is typically used when:

  1. The object creation process is complex and requires multiple steps.
  2. The object’s construction logic should be independent of its representation.
  3. The object should be created with various configurations or representations.

To implement the Builder pattern, follow these steps:

  1. Define a common interface or an abstract class for the builder, which specifies the methods for constructing the complex object.
  2. Create concrete builder classes that implement the common interface or extend the abstract builder class. Each builder should construct a specific representation of the complex object.
  3. Create a director class that takes a builder instance and constructs the complex object using the builder’s methods.
  4. Use the director class to create different representations of the complex object by passing it different builder instances.

Here’s a simple example of the Builder pattern in Java:

// Product class
public class ComplexObject {
private String partA;
private String partB;

public void setPartA(String partA) {
this.partA = partA;
}

public void setPartB(String partB) {
this.partB = partB;
}
}

// Builder interface
public interface Builder {
void buildPartA();
void buildPartB();
ComplexObject getResult();
}

// Concrete builder classes
public class ConcreteBuilder1 implements Builder {
private ComplexObject object = new ComplexObject();

@Override
public void buildPartA() {
object.setPartA("PartA1");
}

@Override
public void buildPartB() {
object.setPartB("PartB1");
}

@Override
public ComplexObject getResult() {
return object;
}
}

public class ConcreteBuilder2 implements Builder {
private ComplexObject object = new ComplexObject();

@Override
public void buildPartA() {
object.setPartA("PartA2");
}

@Override
public void buildPartB() {
object.setPartB("PartB2");
}

@Override
public ComplexObject getResult() {
return object;
}
}

// Director class
public class Director {
private Builder builder;

public void setBuilder(Builder builder) {
this.builder = builder;
}

public void construct() {
builder.buildPartA();
builder.buildPartB();
}
}

// Client code
public class Client {
public static void main(String[] args) {
Director director = new Director();

Builder builder1 = new ConcreteBuilder1();
director.setBuilder(builder1);
director.construct();
ComplexObject object1 = builder1.getResult();

Builder builder2 = new ConcreteBuilder2();
director.setBuilder(builder2);
director.construct();
ComplexObject object2 = builder2.getResult();
}
}

In this example, the `ComplexObject` class represents the complex object to be constructed. The `Builder` interface defines the methods for constructing the object’s parts. The concrete builder classes, `ConcreteBuilder1` and `ConcreteBuilder2`, implement these methods and construct specific representations of the `ComplexObject`. The `Director` class takes a builder instance and constructs the object using the builder’s methods.

Advantages of the Builder pattern:

1. Separation of concerns: The Builder pattern separates the construction of a complex object from its representation, allowing for easier modification or addition of new representations without affecting the construction logic.

2. Flexibility: The pattern allows the creation of different representations of the object by using different builder instances. This can be useful when dealing with objects that require multiple configurations or need to be constructed in different ways.

3. Improved readability: The Builder pattern can make the client code more readable by encapsulating the complex object creation process within the builder and director classes.

Disadvantages of the Builder pattern:

1. Increased complexity: The Builder pattern introduces additional classes and interfaces, which can increase the overall complexity of the code.

2. Redundancy: The pattern may introduce redundancy, as each builder class may need to implement all the methods defined in the builder interface or abstract class, even if some methods are not required for a specific representation.

When using the Builder pattern, consider its benefits and drawbacks carefully. Use the pattern when dealing with complex objects that require a multi-step initialization process, when the object’s construction logic should be decoupled from its representation, or when the object should be created with various configurations or representations. Be aware of the potential complexity and redundancy that may be introduced by the pattern.

Use case: Building a Custom Pizza Order

Sequence diagram for the builder pattern custom pizza order

In this real-time use case, the Builder pattern is employed to create a custom pizza order. A pizza has various attributes such as size, crust, toppings, and cheese. The Builder pattern enables users to easily build a pizza by providing a clear and flexible way to specify the pizza’s attributes.

// Pizza.java
public class Pizza {
private final String size;
private final String crust;
private final String cheese;
private final List<String> toppings;

private Pizza(PizzaBuilder builder) {
this.size = builder.size;
this.crust = builder.crust;
this.cheese = builder.cheese;
this.toppings = builder.toppings;
}

public static class PizzaBuilder {
private final String size;
private String crust;
private String cheese;
private List<String> toppings = new ArrayList<>();

public PizzaBuilder(String size) {
this.size = size;
}

public PizzaBuilder crust(String crust) {
this.crust = crust;
return this;
}

public PizzaBuilder cheese(String cheese) {
this.cheese = cheese;
return this;
}

public PizzaBuilder addTopping(String topping) {
this.toppings.add(topping);
return this;
}

public Pizza build() {
return new Pizza(this);
}
}

@Override
public String toString() {
return "Pizza{" +
"size='" + size + '\'' +
", crust='" + crust + '\'' +
", cheese='" + cheese + '\'' +
", toppings=" + toppings +
'}';
}
}

// Main.java
public class Main {
public static void main(String[] args) {
Pizza pizza = new Pizza.PizzaBuilder("Large")
.crust("Thin")
.cheese("Mozzarella")
.addTopping("Pepperoni")
.addTopping("Mushrooms")
.addTopping("Onions")
.build();

System.out.println(pizza);
}
}

In this example, the Pizza class contains the pizza's attributes (size, crust, cheese, and toppings) and a nested PizzaBuilder class. The PizzaBuilder class provides methods for specifying each attribute, making it easy to build a custom pizza. The main method demonstrates how to create a pizza using the Builder pattern.

Class Diagram for this Custom Pizza order

Note: For complete list of design patterns click here

--

--