Skip to content

Builder Design Pattern

Overview

A builder patter an alternative way to construct complex objects.

This pattern should be used when building different immutable objects using the same object building process.

GoF Builder Pattern

The builder pattern is a design pattern that allows for the step-by-step creation of complex objects using the correct sequence of actions. The construction is controlled by a director object that only needs to know the type of object it is to create. - Gang Of Four

Builder_UML_class_diagram.png The GoF builder pattern is very much similar to the abstract factory pattern, where we use a factory or builder for a specific type of object, then the factory returns a concrete instance of that object.

The big difference between the builder pattern and the abstract factory pattern is that the builder provides more control over the object creation process.

An improved definition

A builder pattern aims to separate the construction of a complex object from its representation so that the same construction process can create multiple different representations.

A builder pattern should act more like a fluent interface. A fluent interface is normally implemented by using method cascading / method chaining as see in lambda expressions.

Telescoping Constructors

Telescoping Constructors can occur when constructors within a class. For example, the following code block is used to set the immutable values for a User object.

Java Object
public User (String firstName, String lastName, int age, String phone, String address){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.phone = phone;
    this.address = address;
}

Now if only firstName and lastName are mandatory and the rest 3 fields are optional. We need more constructors. This problem is called the telescoping constructors problem. In this scenario, the builder pattern would help to consume additional attributes whilst retaining the immutability of the class.

Telescoping Constructors
public User (String firstName, String lastName, int age, String phone){ ... }
public User (String firstName, String lastName, String phone, String address){ ...  }
public User (String firstName, String lastName, int age){ ...   }
public User (String firstName, String lastName){ ...    }

Within Object Declaration

Telescoping constructors can also occur when an object has multiple fields passed to it. As seen in the example below, each object has an expected number of fields to parse, however, using a constructor to pass these fields makes the code hard to read.

Telescoping Constructors
            PersonalDetails personalDetails = new PersonalDetails(new Name(Title.valueOf(arrayItem[TITLE]),arrayItem[FIRST_NAME],arrayItem[LAST_NAME]), LocalDate.parse(arrayItem[DATE_OF_BIRTH]));
            ContactDetails contactDetails = new ContactDetails(new Address(arrayItem[ADDRESS_FIRST_LINE], arrayItem[ADDRESS_SECOND_LINE], arrayItem[ADDRESS_THIRD_LINE], arrayItem[POSTCODE]), new PhoneNumber(arrayItem[PHONE_HOME], arrayItem[PHONE_MOBILE], arrayItem[PHONE_WORK]), new EmailAddress(arrayItem[EMAIL_ADDRESS]));

Implementation

Using the User scenario above, a builder pattern could be built out as follows:

Builder Implementation

User.java
public class User {
    ///////////////////////////////////////////
    // All Fields / Attributes are defined here
    ///////////////////////////////////////////
    private final String firstName; // required
    private final String lastName; // required
    private final int age; // optional
    private final String phone; // optional
    private final String address; // optional

    ///////////////////////////////////////////
    // Create a function that accepts the fields of the object
    // Accepts type 'userBuilder' and variable 'builder'
    // This can only be called from within this class
    ///////////////////////////////////////////
    private User(userBuilder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phone = builder.phone;
        this.address = builder.address;
    }

    ///////////////////////////////////////////
    // Create getters
    // No setters are created, this provides immutability for the created object
    ///////////////////////////////////////////
    public String getFirstName() {
        return firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public int getAge() {
        return age;
    }
    public String getPhone() {
        return phone;
    }
    public String getAddress(){
        return address;
    }

    ///////////////////////////////////////////
    // Include an override for toString to print some data out to test later
    ///////////////////////////////////////////
    @Override
    public String toString() {
        return "User: "+this.firstName+", "+this.lastName+", "+this.age+", "+this.phone+", "+this.address;
    }

    ///////////////////////////////////////////
    // Create the UserBuilder class
    // It can be called from anywhere as it is public
    ///////////////////////////////////////////
    public static class UserBuilder {

        ///////////////////////////////////////////
        // Create fields for the UserBuilder class
        // These fields are the same as the ones first declared at the start of the file
        ///////////////////////////////////////////
        private final String firstName;
        private final String lastName;
        private final int age;
        private final String phone;
        private final String address;

        ///////////////////////////////////////////
        // Create a function that takes the first and last name as arguments
        // These are then set the the relevant variable
        // !! Note, these have to be defined when the UserBuilder function is called
        ///////////////////////////////////////////
        public UserBuilder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        ///////////////////////////////////////////
        // Create functions, using the one above as an example
        // for the other fields that have been declared
        // !! Note that these have a variable name associated with them
        // !! By doing so, they can be called later by .age, .phone, etc
        ///////////////////////////////////////////
        public UserBuilder age (int age) {
            this.age = age;
            // return this; returns the value of the parsed field into the builder object
            return this;
        }
        public UserBuilder phone (String phone) {
            this.phone = phone;
            return this;
        }
        public UserBuilder address (String address) {
            this.address = address;
            return this;
        }

        ///////////////////////////////////////////
        // Use the build() function to build a fully constructed User object
        // Set the new object to take the values of 'this' object as input
        // Then return the new object back to the function this was called from
        ///////////////////////////////////////////
        public User build(){
            User user = new User(this);
            validateUserObject(user); // Pass the user object to the validationUserObject function
            return user;
        }

        ///////////////////////////////////////////
        // A validation function can be used to ensure that the object does not contain
        // malformed data or anything that may cause an exception to be thrown/
        /////////////////////////////////////////// 
        private void validateUserObject(User user) {
            ; // This does nothing for now.
        }
}

Builder Usage

public static void main(String args[]) {
    User user1 = new User.UserBuilder("Homer", "Simpson")
        .age(32)
        .phone("101-555-404")
        .address("Evergreen Terrace")
        .build();

    System.out.println(user1);
    // Output - User: Homer, Simpson, 32, 101-555-404, Evergreen Terrace

    User user2 = new User.UserBuilder("Marge", "Simpson")
        // skip age
        // skip phone
        .address("Evergreen Terrace")
        .build();

    // Output - User: Marge, Simpson, , , Evergreen Terrace
}

Note

The above-created User object does not have any setter method, so its state can not be changed once it has been built. This provides the desired immutability.

When adding a new attribute and containing the source code changes to a single class, it is worth enclosing the builder inside the class (as in the above example). It makes the change more obvious that there is a relevant builder that needs to be updated too.

Advantages

The number of lines of code increases at least to double in the builder pattern, but the effort pays off in terms of design flexibility and much more readable code.

The parameters to the constructor are reduced and are provided in highly readable chained method calls. This way there is no need to pass in null for optional parameters to the constructor while creating the instance of a class.

Another advantage is that an instance is always instantiated in a complete state rather than sitting in an incomplete state until a setter is called.

Disadvantages

Larger code base, at the cost of increased readability.