Design Patterns - Builder (in Java)

Design Patterns - Builder (in Java)

When readability and flexibility matters

Definition

Do you ever feel tired, of setting so many properties during an object creation? One of the most used creational design patterns is the Builder Design Pattern provides flexibility and increases the readability of the object creation process.

Idea Behind Builder Design Pattern

  • Take help from a Builder class to build an object of Class C

    • Don’t use a constructor and setters in class C to set properties directly
  • All the setters in Builder should return Builder for chaining

Towards the Builder Pattern - Step by Step

Suppose we have a Car class with lots of properties:

public class Car {
    // Required parameters
    private final String engine;
    private final String transmission;
    private final String bodyStyle;

    // Optional parameters
    private final String color;
    private final String interior;
    private final boolean sunroof;
    private final boolean navigationSystem;
    private final boolean parkingSensors;
    private final boolean heatedSeats;
    private final boolean bluetooth;

    public Car(String engine, String transmission, String bodyStyle, String color, String interior, 
               boolean sunroof, boolean navigationSystem, boolean parkingSensors, 
               boolean heatedSeats, boolean bluetooth) {
        this.engine = engine;
        this.transmission = transmission;
        this.bodyStyle = bodyStyle;
        this.color = color;
        this.interior = interior;
        this.sunroof = sunroof;
        this.navigationSystem = navigationSystem;
        this.parkingSensors = parkingSensors;
        this.heatedSeats = heatedSeats;
        this.bluetooth = bluetooth;
    }

    @Override
    public String toString() {
        return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle + 
               ", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof + 
               ", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors + 
               ", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car("V8", "Automatic", "SUV", "Red", "Leather", true, true, true, true, true);
        System.out.println(car);
    }
}
  • Just think about the Car constructor! Remembering all the parameter sequences we passed during object creation is hard!

  • The code loses readability.

  • It also forces us to pass the optional parameters, which are not mandatory.

Attempt 01: Overloaded Constructors

We may think of creating overloaded constructors with different combinations of optional parameters.

  •             public Car(String engine, String transmission, String bodyStyle,
                        String color) {
                        this.engine = engine;
                        // ... ... ...
                }
                public Car(String engine, String transmission, String bodyStyle,
                        String color, String interior) {
                        this.engine = engine;
                        // ... ... ...
                }
                public Car(String engine, String transmission, String bodyStyle,
                        String color, String interior, boolean bluetooth) {
                        this.engine = engine;
                        // ... ... ...
                }
                // ... ... ...
    

Don’t you think this is the worst design in history for creating an object? It is not easy to understand or remember what parameters we will pass and which constructor will be called! This will be a disaster!

Attempt 02: Using setter() Methods

public class Car {
    // Required parameters
    private String engine;
    private String transmission;
    private String bodyStyle;

    // Optional parameters
    private String color;
    private String interior;
    private boolean sunroof;
    private boolean navigationSystem;
    private boolean parkingSensors;
    private boolean heatedSeats;
    private boolean bluetooth;

    public Car(String engine, String transmission, String bodyStyle) {
        this.engine = engine;
        this.transmission = transmission;
        this.bodyStyle = bodyStyle;
    }

    // Setters for optional parameters
    public void setColor(String color) {
        this.color = color;
    }

    public void setInterior(String interior) {
        this.interior = interior;
    }

    public void setSunroof(boolean sunroof) {
        this.sunroof = sunroof;
    }

    public void setNavigationSystem(boolean navigationSystem) {
        this.navigationSystem = navigationSystem;
    }

    public void setParkingSensors(boolean parkingSensors) {
        this.parkingSensors = parkingSensors;
    }

    public void setHeatedSeats(boolean heatedSeats) {
        this.heatedSeats = heatedSeats;
    }

    public void setBluetooth(boolean bluetooth) {
        this.bluetooth = bluetooth;
    }

    @Override
    public String toString() {
        return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle + 
               ", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof + 
               ", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors + 
               ", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car("V8", "Automatic", "SUV");
        car.setColor("Red");
        car.setInterior("Leather");
        car.setSunroof(true);
        car.setNavigationSystem(true);
        car.setParkingSensors(true);
        car.setHeatedSeats(true);
        car.setBluetooth(true);

        System.out.println(car);
    }
}

We can pass mandatory parameters in the constructor and keep setter methods to set the optional parameters if necessary.

But, If another thread tries to use the Car object before all setters are called, it may see an incomplete object. So this approach is not thread-safe!

Attempt 03: Builder is the Solution

public class Car {
    // Required parameters
    private final String engine;
    private final String transmission;
    private final String bodyStyle;

    // Optional parameters
    private final String color;
    private final String interior;
    private final boolean sunroof;
    private final boolean navigationSystem;
    private final boolean parkingSensors;
    private final boolean heatedSeats;
    private final boolean bluetooth;

    private Car(Builder builder) {
        this.engine = builder.engine;
        this.transmission = builder.transmission;
        this.bodyStyle = builder.bodyStyle;
        this.color = builder.color;
        this.interior = builder.interior;
        this.sunroof = builder.sunroof;
        this.navigationSystem = builder.navigationSystem;
        this.parkingSensors = builder.parkingSensors;
        this.heatedSeats = builder.heatedSeats;
        this.bluetooth = builder.bluetooth;
    }

    @Override
    public String toString() {
        return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle + 
               ", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof + 
               ", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors + 
               ", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
    }

    // Builder Class
    public static class Builder {
        // Required parameters
        private final String engine;
        private final String transmission;
        private final String bodyStyle;

        // Optional parameters
        private String color;
        private String interior;
        private boolean sunroof;
        private boolean navigationSystem;
        private boolean parkingSensors;
        private boolean heatedSeats;
        private boolean bluetooth;

        // All the mandatory parameters:
        public Builder(String engine, String transmission, String bodyStyle) {
            this.engine = engine;
            this.transmission = transmission;
            this.bodyStyle = bodyStyle;
        }

        public Builder color(String color) {
            this.color = color;
            return this;
        }

        public Builder interior(String interior) {
            this.interior = interior;
            return this;
        }

        public Builder sunroof(boolean sunroof) {
            this.sunroof = sunroof;
            return this;
        }

        public Builder navigationSystem(boolean navigationSystem) {
            this.navigationSystem = navigationSystem;
            return this;
        }

        public Builder parkingSensors(boolean parkingSensors) {
            this.parkingSensors = parkingSensors;
            return this;
        }

        public Builder heatedSeats(boolean heatedSeats) {
            this.heatedSeats = heatedSeats;
            return this;
        }

        public Builder bluetooth(boolean bluetooth) {
            this.bluetooth = bluetooth;
            return this;
        }

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

public class Main {
    public static void main(String[] args) {
        Car car = new Car.Builder("V8", "Automatic", "SUV")
                .color("Red")
                .interior("Leather")
                .sunroof(true)
                .navigationSystem(true)
                .parkingSensors(true)
                .heatedSeats(true)
                .bluetooth(true)
                .build();
        System.out.println(car);
    }
}
  • Car doesn’t expose the public constructors or setters

  • Builder has the same number of properties, Car has

  • Builder constructor receives all the mandatory properties

  • To set the optional properties Builder exposes public methods with Builder return type (this helps us chaining)

  • finally build() method helps us to build the Car object

The code becomes a bit complex but we can gain a lot of benefits from it.

Don’t Just Accept, Ask the Questions!

Let’s observe the builder pattern from a different angle.

What if we remove the helper Builder class and implement the same functionality by the Car class itself?

public class Car {
    // Required parameters
    private final String engine;
    private final String transmission;
    private final String bodyStyle;

    // Optional parameters
    private String color;
    private String interior;
    private boolean sunroof;
    private boolean navigationSystem;
    private boolean parkingSensors;
    private boolean heatedSeats;
    private boolean bluetooth;

    public Car(String engine, String transmission, String bodyStyle) {
        this.engine = engine;
        this.transmission = transmission;
        this.bodyStyle = bodyStyle;
    }

    public Car color(String color) {
        this.color = color;
        return this;
    }

    public Car interior(String interior) {
        this.interior = interior;
        return this;
    }

    public Car sunroof(boolean sunroof) {
        this.sunroof = sunroof;
        return this;
    }

    public Car navigationSystem(boolean navigationSystem) {
        this.navigationSystem = navigationSystem;
        return this;
    }

    public Car parkingSensors(boolean parkingSensors) {
        this.parkingSensors = parkingSensors;
        return this;
    }

    public Car heatedSeats(boolean heatedSeats) {
        this.heatedSeats = heatedSeats;
        return this;
    }

    @Override
    public String toString() {
        return "Car [engine=" + engine + ", transmission=" + transmission + ", bodyStyle=" + bodyStyle + 
               ", color=" + color + ", interior=" + interior + ", sunroof=" + sunroof + 
               ", navigationSystem=" + navigationSystem + ", parkingSensors=" + parkingSensors + 
               ", heatedSeats=" + heatedSeats + ", bluetooth=" + bluetooth + "]";
    }
    // all the getters if required ...
}
public class Main {
    public static void main(String[] args) {
        Car myCar = new Car("V8", "Automatic", "SUV")
                .color("Red")
                .interior("Leather")
                .sunroof(true)
                .navigationSystem(true)
                .parkingSensors(true)
                .heatedSeats(true)
                .bluetooth(true);
    }
}

We can achieve the same functionality - right?

- Wrong!

Another characteristic of a Builder pattern is that it makes your object immutable. This means you can only set your properties during the object creation time; after that, you can’t update your object accidentally.

From the above example, myCar can be updated again after the object is created!

Benefits of Builder Pattern

  • Improved readability and maintainability of our code

  • Flexible enough to create complex objects

  • The final object is immutable, helps prevent accidental changes

  • As the object creation does not involve multiple setters, it will not be in an inconsistent state

One Last Note

Be aware of using the Builder pattern, for objects with only a few properties or where all properties are mandatory, the Builder Pattern might not provide significant benefits. For example:

A Point class with x and y coordinates might not need a builder.