Builder Design Pattern

Builder Design Pattern

Introduction

The Builder Design Pattern is a creational design pattern used in software development. It allows for the step-by-step creation of immutable, complex objects using a simple and understandable interface. With the Builder Pattern, a builder object is used to create an object. The builder object has methods for setting different properties of the object it is creating and finally a method for returning the created object.

Let's consider we want to create a User class with a few mandatory fields like firstName and age along with a few non-mandatory fields like lastName and country. Using a constructor without setters for making immutable objects can be a viable option but it will become increasingly difficult to maintain as the number of parameters increases.

To handle this problem we can use the builder design pattern. The Builder design pattern provides an easy way to create immutable objects with mandatory and non-mandatory fields.

Builder Pattern Prior to Java 8

Steps:

  1. Define a static inner class UserBuilder within the outer class User.

  2. In the outer class, create a private constructor so it can be instantiated only through the inner class.

  3. The inner class should have the same fields as the outer class. It should have constructors to set mandatory fields and setters to set non-mandatory fields. Setters should return an instance of the inner class.

  4. The inner class should also have a method (build) which uses fields from the inner class to return an instance of the outer class.

class User {
  private String firstName;
  private String lastName;
  private String country;
  private int age;

  // private constructor so that User object can only be instantiated by UserBuilder
  private User() {}

  // Add Getters

  @Override
  public String toString() {
    return "User{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", country='" + country
        + '\'' + ", age=" + age + '}';
  }

  public static class UserBuilder {
    private String firstName;
    private String lastName;
    private String country;
    private int age;

    // Mandatory fields are provided in Constructor of builder
    public UserBuilder(String firstName, int age) {
      this.firstName = firstName;
      this.age = age;
    }

    // Non mandatory fields are set using setters
    public UserBuilder lastName(String lastName) {
      this.lastName = lastName;
      return this;
    }

    public UserBuilder country(String country) {
      this.country = country;
      return this;
    }

    // build method is used to create user object
    public User build() {
      User user = new User();
      user.firstName = firstName;
      user.lastName = lastName;
      user.country = country;
      user.age = age;
      return user;
    }
  }

}

The client can use the User class as below:

public static void main(String[] args) {
    User user = new User.UserBuilder("John", 20) // mandatory fields
        .lastName("Doe") // non mandatory field
        .country("USA")  // non mandatory field
        .build();
    System.out.println("Created user: " + user);

    User Bob = new User.UserBuilder("Bob", 21)
        .lastName("Builder")
        .build();
    System.out.println("Created Bob: " + Bob);
}

Builder Pattern in Java 8

Java 8 comes with functional interfaces and lambda expressions. We can use Consumer functional interface to take care of setting values in the Builder class.

Steps:

  1. Define a static inner class LambdaBuilder within the outer class User.

  2. In the outer class, create a private constructor so it can be instantiated only through the inner class.

  3. The inner class should have a method that accepts a Consumer of the inner class. This method is practically a replacement for the constructor and setters. The consumer takes care of setting all the fields in the inner class.

  4. The inner class should also have a method (build) which uses fields from the inner class and creates an instance of the outer class.

class User {
  private String firstName;
  private String lastName;
  private String country;
  private int age;

  // private constructor so that User object can only be instantiated by LambdaBuilder
  private User() {}

  // Getters

  @Override
  public String toString() {
    return "User{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", country='" + country
        + '\'' + ", age=" + age + '}';
  }

  public static class LambdaBuilder {
    public String firstName;
    public String lastName;
    public int age;
    public String country;

    // This method uses the provided consumer to set data in Builder
    public LambdaBuilder with(Consumer<LambdaBuilder> builderFunction){
      builderFunction.accept(this);
      return this;
    }

    // build method is used to create User Object
    public User build() {
      User user = new User();
      user.firstName = firstName;
      user.lastName = lastName;
      user.country = country;
      user.age = age;
      return user;
    }
  }

}

The client can use the User class as below:

public class Main {
  public static void main(String[] args) {
    User charlie = new User.LambdaBuilder().with(user -> {
      user.firstName = "Charlie";
      user.lastName = "Hunnam";
      user.country = "USA";
      user.age = 30;
    }).build();
    System.out.println("Created Charlie: " + charlie);
  }
}

Advantages

  1. Allows for a more organized and efficient way of creating complex objects.

  2. Makes code more maintainable and extensible by allowing for the creation of classes that can be easily modified and extended.

  3. Makes code more reliable by ensuring that all the necessary components are included in the object.

  4. Reduces the amount of code that needs to be written by allowing developers to reuse components.

Disadvantages

  1. Can be difficult to debug if an incorrect combination of components is used.

  2. Requires more effort to design and create the required classes.

  3. Can be time-consuming to create all the necessary components.