Creational Design Patterns : Builder

The Builder design pattern illustrated with Java

This is the second part of a series I'm writing on creational design patterns, you can find the first here. Now, without any further ado, let's get right to it.

The Builder pattern

The Builder pattern is particularly useful when dealing with complex objects or classes with a lot of constructors or a high amount of constructor arguments.

The Builder pattern creates an object by building it in step by step approach, the attributes of the object are added as needed then a final method is called to create and return the new object.

It helps you create different representations of an object using the same construction code.

I know I said the Builder is best for complex classes, but for this example, we are going to implement a Builder with a simple easy-to-understand example, but the concepts remain the same.

Implementing a Builder using Java

Let's outline the components we need to implement a builder.

  1. The outer class (the class whose objects we intend to be creating).

  2. A private constructor for our outer class so the instantiation of new objects is delegated to the builder exclusively, the builder object is passed to the constructor so the target object can be created.

  3. Private fields to be accessed only through getter methods.

  4. Public getter methods to return attributes.

  5. The builder static inner class.

  6. A public constructor for the builder class to accept and set the compulsory attributes (if any).

  7. Public methods to set each attribute on the builder object. Notice that the methods return the builder object, this is what allows us to build the same builder object step by step, enabling us to chain these methods.

  8. The build() method, which calls the constructor of the outer class, passing in the builder we just built.

See implementation below:

public class Car {
    private String make;
    private String model;
    private int year;
    private String color;
    private boolean hasHeatedSeats; //optional 
    private int maxSpeed; //optional 
    private boolean hasMoonRoof; //optional 

    public String getMake() {
        return make;
    }

    public String getModel() {
        return model;
    }

    public int getYear() {
        return year;
    }

    public String getColor() {
        return color;
    }

    public boolean hasHeatedSeats() {
        return hasHeatedSeats;
    }

    public int getMaxSpeed() {
        return maxSpeed;
    }

    public boolean hasMoonRoof() {
        return hasMoonRoof;
    }

    private Car(CarBuilder builder) {
        this.make = builder.make;
        this.model = builder.model;
        this.year = builder.year;
        this.color = builder.color;
        this.hasHeatedSeats = builder.hasHeatedSeats;
        this.maxSpeed = builder.maxSpeed;
        this.hasMoonRoof = builder.hasMoonRoof;
    }

    public static class CarBuilder{
        private String make;
        private String model;
        private int year;
        private String color;
        private boolean hasHeatedSeats;
        private int maxSpeed;
        private boolean hasMoonRoof;

        public CarBuilder(String make, String model, int year, String color){
            this.make = make;
            this.model = model;
            this.year = year;
            this.color = color;
        }

        public CarBuilder setMake(String make){
            this.make = make;
            return this;
        }
        public CarBuilder setModel(String model){
            this.model = model;
            return this;
        }
        public CarBuilder setYear(int year){
            this.year = year;
            return this;
        }
        public CarBuilder setColor(String color){
            this.color = color;
            return this;
        }
        public CarBuilder setHeatedSeats(boolean hasHeatedSeats){
            this.hasHeatedSeats = hasHeatedSeats;
            return this;
        }
        public CarBuilder setMaxSpeed(int maxSpeed){
            this.maxSpeed = maxSpeed;
            return this;
        }
        public CarBuilder setHasMoonRoof(boolean hasMoonRoof){
            this.hasMoonRoof = hasMoonRoof;
            return this;
        }
        public Car build(){
            return new Car(this);
        }
    }
}

Our Car class is set up in a way that we have some compulsory attributes: make, model, year, and color . We also have some optional attributes: hasHeatedSeats, maxSpeed and hasMoonRoof. Our builder class is able to build car objects with the compulsory attributes and any combination of the optional attributes, that's the beauty of the builder pattern: being able to create multiple representations of an object using the construction code.

If you are implementing a class, say Person. It's conventional to name your builder class PersonBuilder, basically append Builder to the end of your class name.

Now that we have our builder class set, we are now ready to build cars! See how to use the builder we created below.

 public class CarDemo {
    public static void main (String [] args){
        //Building a car with default specs
        Car basicCar = new Car.CarBuilder("Toyota", "Highlander", 2019, "Grey")
                        .build();

        //Building another car with default specs + some optional specs
        Car deluxeCar = new Car.CarBuilder("Toyota", "Highlander", 2019, "Grey")
                .setHasMoonRoof(true)
                .setMaxSpeed(300)
                .setHeatedSeats(true)
                .build();
    }
}

We the example we implemented, you see that we were able to create different types of cars using the same builder construction logic.

Benefits of using the Builder pattern

Simplified complex object creation: The builder pattern simplifies the creation of complex objects, it achieves this by separating the construction logic from the representation of the objects, this is evident in complex objects.

Reduced number of constructors: The builder pattern eliminates the need to create multiple constructors to cater to different representations of objects.

Improved code quality: Assuming you have one huge constructor that accepts compulsory and optional arguments, you might instantiate an object like this :

var employee = new Employee("James", "Maddison", "M", null, null, "Finance", "2023-09-12", null, "12345", null, null, null, "6330, Willow Blvd");

Passing nulls like this is a bad practice, if your constructor looks like this then it's time to switch to a builder! Or, worst case, overload the constructor by creating other constructor methods.

Conclusion

This concludes our journey into the builder pattern world, with this pattern, you can manage the construction of complex objects step by step, improving code readability and the overall structure of your codebase.

If you are not feeling like implementing your own builder class, you can make use of the project Lombok library, with this, all you need to do is annotate your class with their builder annotation and voila, your class has a builder, this post would help you understand what they implemented under the hood.

On a final note, do a review of your codebases and see if you need to do some refactoring!