Builder Design Pattern in Java: Mastering Complex Object Construction

Builder Design Pattern in Java: Mastering Complex Object Construction

Creational Design Patterns in Java (GOF)

• Introduction:

In software development, constructing complex objects with multiple configuration options can be challenging. the Builder design pattern comes to the rescue by offering an elegant solution. By decoupling the construction process from the object's representation, this design pattern eliminates the need for multiple constructors with different parameter combinations. In this comprehensive article, we will delve into the world of the Builder design pattern in Java. We will unravel its inner workings and discover how it streamlines the creation of intricate objects, providing developers with enhanced simplicity and flexibility. Join us as we explore this powerful pattern and unlock its potential in crafting sophisticated objects effortlessly.

Overview:

The Builder design pattern is a creational pattern that allows for step-by-step construction of complex objects. It consists of four main components: the Product, Builder, ConcreteBuilder, and Director. The Product represents the final object, while the Builder abstracts the construction process. The ConcreteBuilder provides specific implementations of the Builder interface, and the Director controls the construction process using the Builder.

Example Implementation:

inner Builder class:

Let's consider an example where we want to construct a House object with various optional attributes such as the number of floors, the presence of a garden, and the type of roof.

 class  Car{
    private String make;
    private String model;
    private int year;
    private boolean hasSunroof;
    private boolean hasNavigation;

    public  Car(Builder builder){
        this.make = builder.make;
        this.model = builder.model;
        this.year = builder.year;
        this.hasSunroof = builder.hasSunroof;
        this.hasNavigation = builder.hasNavigation;
    }

    // The Builder class is responsible for constructing the car object.
    // It provides methods to set the car's attributes.
    public static class Builder{
        private String make;
        private String model;
        private int year;
        private boolean hasSunroof;
        private boolean hasNavigation;
        //constructor
        public Builder(){}
        // getters & setters
        public Builder setHasNavigation(boolean hasNavigation) {
            this.hasNavigation = hasNavigation;
            return this;
        }
        public Builder setHasSunroof(boolean hasSunroof) {
            this.hasSunroof = hasSunroof;
            return this;
        }
        public Builder setMake(String make) {
            this.make = make;
            return this;
        }

        public Builder setModel(String model) {
            this.model = model;
            return this;
        }
        public Builder setYear(int year) {
            this.year = year;
            return this;
        }
        /**
         * Constructs the car object with the provided attributes.
         * @return The constructed Car object.
         */
        public Car build(){
            return new Car(this);
        }
    }
    @Override
    public String toString() {
        return "Car{" +
                "make='" + make + '\'' +
                ", model='" + model + '\'' +
                ", year=" + year +
                ", hasSunroof=" + hasSunroof +
                ", hasNavigation=" + hasNavigation +
                '}';
    }
}

The Car class represents a car object with attributes such as make, model, year, hasSunroof, and hasNavigation. It implements the Builder Pattern to allow flexible construction of car objects with optional parameters. The inner class Builder provides methods to set the car's attributes and constructs the car object using the Car constructor.

external Builder class:

Now, let's take a step further and decouple the process of constructing an object from its representation. By doing so, we can achieve greater flexibility and clarity in our code, making it easier to construct complex objects with various configuration options

  • Car class:
class Car {
    private String make;
    private String model;
    private int year;
    private boolean hasSunroof;
    private boolean hasNavigation;
    //Car constructor should not be public if you want to force clients classes to use carBuilder class to instantiate it.
    Car(String make, String model, int year, boolean hasSunroof, boolean hasNavigation) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.hasSunroof = hasSunroof;
        this.hasNavigation = hasNavigation;
    }

    //setters
}
  • carBuilder class:
class CarBuilder {
    private String make;
    private String model;
    private int year;
    private boolean hasSunroof;
    private boolean hasNavigation;
    //constructor
    public CarBuilder() {
    }
    //setters
    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 setHasSunroof(boolean hasSunroof) {
        this.hasSunroof = hasSunroof;
        return this;
    }

    public CarBuilder setHasNavigation(boolean hasNavigation) {
        this.hasNavigation = hasNavigation;
        return this;
    }

    /**
    * Constructs the car object with the provided attributes.
    * @return The constructed Car object.
    */
    public Car build() {
        return new Car(make, model, year, hasSunroof, hasNavigation);
    }
}
  • Usage:
    public static void main(String[] args) {

        //1st way
        CarBuilder builder = new CarBuilder();
        builder.setMake("Tesla"); 
        builder.setModel("Model S");
        builder.setYear(2022); 
        builder.setHasSunroof(true);
        builder.setHasNavigation(true);
        Car car = builder.build();

        // 2nd way
        Car car2 = new CarBuilder().setMake("Tesla").setModel("Model S").setYear(2022).setHasSunroof(true).build();

        // Use the constructed car object
        System.out.println("Make: " + car.getMake());
        System.out.println("Model: " + car.getModel());
        //...
    }

Suppose now we have multiple objects sharing certain similarities like multiple vehicles belong to the same company, model, year …

So the builder Pattern suggests that this repeated series of calls should be extracted to a separate class we call Director, The Director class defines the order in which we should call the construction steps so that we can reuse specific configurations of products we are building.

First, let’s use carBuilder as an interface to allow Director to create multiple implementations of cars.

    1. Create carBuilder interface:

         interface CarBuilder {
             CarBuilder setMake(String make);
             CarBuilder setModel(String model);
             CarBuilder setYear(int year);
             CarBuilder setHasSunroof(boolean hasSunroof);
             CarBuilder setHasNavigation(boolean hasNavigation);
             Car build();
         }
      
      1. Create Implementation of car builder:

        class CarBuilderImpl implements CarBuilder {
            private String make;
            private String model;
            private int year;
            private boolean hasSunroof;
            private boolean hasNavigation;
        
            public CarBuilder setMake(String make) {
                this.make = make;
                return this;
            } 
            //set model , year ... with same way
        
            /**
            * Constructs the car object with the provided attributes.
            * @return The constructed Car object.
            */
            public Car build() {
                return new Car(make, model, year, hasSunroof, hasNavigation);
            }
        }
        
      2. Create director:

        class CarDirector {
            private CarBuilder builder;
        
            public CarDirector(CarBuilder builder) {
                this.builder = builder;
            }
        
            // Constructs a standard car using the builder
            public Car constructStandardCar() {
                return builder.setMake("Tesla")
                        .setModel("Model S")
                        .setYear(2022)
                        .setHasSunroof(false)
                        .setHasNavigation(true)
                        .build();
            }
        
            // Constructs a premium car using the builder
            public Car constructPremiumCar() {
                return builder.setMake("Mercedes")
                        .setModel("S-Class")
                        .setYear(2022)
                        .setHasSunroof(true)
                        .setHasNavigation(true)
                        .build();
            }
        }
        

        By utilizing the CarDirector class, we can easily create different variations of vehicles without exposing the complex construction process to the client code. The client only needs to interact with the director and obtain the fully constructed objects.

      3. Usage:

  • Benefits of the Builder Pattern:

The Builder pattern offers several benefits:

✅Improved readability: The method chaining approach provides a clear and readable syntax for constructing objects with optional attributes.

✅Flexible object construction: The Builder pattern allows the creation of objects with varying configurations without the need for multiple constructors or complex parameters.

✅Enhanced Maintainability*:* As the number of attributes or configuration options increases, managing constructors or setter methods becomes more cumbersome. The Builder pattern centralizes the construction logic within the builder class, making the code easier to maintain and modify.

✅Immutable Objects: The Builder pattern promotes immutability by constructing the object in a step-by-step manner. Once the object is constructed, its state cannot be modified, ensuring thread safety and preventing unexpected changes.

Source Code of this article:

I'm Active in

Youtube

Instagram

Gmail

ismail harik