Builder Design Pattern. What? Why? How?

Builder Design Pattern. What? Why? How?

As an Android Developer, we use and implement various libraries in our project, and often encounter a specific creational pattern called Builder Design Pattern.

In this blog, we will discuss what is builder design pattern, what problem it solves and how can we use it in our projects.

The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The builder design pattern intends to separate the construction of a complex object from its representation. It is one of the Gang of Four design patterns.

But Why do we need it in the first place?

Let's suppose we want to create an Object called Person with some mandatory and optional properties.
Sometimes a user may pass only a few optional parameters, with the traditional way we would do something like this:

class Person {
    private var firstName:String
    private var lastName: String? = null    //optional
    private var email: String? = null       //optional
    private var age: Int? = null            //optional
    private var gender: String? = null      //optional

    constructor(firstName : String, age : Int?){
        this.firstName = firstName
        this.age = age
    }

    constructor(firstName : String, email : String?){
        this.firstName = firstName
        this.email = email
    }

    constructor(firstName : String, lastName : String){
        this.firstName = firstName
        this.lastName = lastName
    }

    constructor(firstName : String,lastName : String, age: Int?){
        this.firstName = firstName
        this.lastName = lastName
        this.age=age
    }

    constructor(firstName : String,lastName : String, age: Int?,gender: String?){
        this.firstName = firstName
        this.lastName = lastName
        this.age=age
        this.gender = gender
    }

}

Here we are using Overloaded Constructor so that when we create the Person Object we could pass the parameters based on our use case.

However, this creates several issues:

  1. We must write all the possible permutations and combinations of the constructors.

  2. It violates the Don’t Repeat Yourself (DRY) software principle as a lot of code is repeated in the constructors.

  3. It becomes difficult to add or remove parameters from the constructors as it would affect a lot of code which could lead to compilation errors.

  4. Creates confusion for users while declaring the Object.

To resolve these issues we could use some classical approaches like using Telescoping Constructors or Using Setters.

But both of the above approaches don't fully resolve all our problems.

For example, if we use Telescoping Constructors, the code repetition is resolved but still we need multiple constructors. If we use Setters, then the object becomes Mutable.


Now Let's discuss how we can come up with a solution that resolves both of our issues making the Object Immutable and setting the values using setters.

First, let's create an Immutable Class called Person

class Person(
    val firstName: String,
    val lastName: String? = null, // optional
    val email: String? = null,    // optional
    val age: Int? = null,         // optional
    val gender: String? = null    // optional
) {
   // Immutable Class Person
}

Now we create a new class MutablePerson with default constructor and setter functions.

Here we add a new method called createImmutablePerson() which returns the Person Object.

class MutablePerson {

    var firstName: String? = null
    var lastName: String? = null  // optional
    var email: String? = null    // optional
    var age: Int? = null         // optional
    var gender: String? = null

    constructor()

    fun setFirstName(firstName: String) {
        this.firstName = firstName
    }

    fun setLastName(lastName: String) {
        this.lastName = lastName
    }

    fun setEmail(email: String) {
        this.email = email
    }

    fun setAge(age: Int) {
        this.age = age
    }

    fun setGender(gender: String) {
        this.gender = gender
    }

    fun createImmutablePerson() : Person {
        return Person(
            firstName!!,
            lastName,
            email,
            age,
            gender
        )
    }

}

By following the above approach we were able to create an Immutable Object Person.

And also use setter functions thus solving both of our problems.

Lastly, rename the MutablePerson class to Builder and createImmutablePerson() to build().

class Builder {

    var firstName: String? = null
    var lastName: String? = null  // optional
    var email: String? = null    // optional
    var age: Int? = null         // optional
    var gender: String? = null

    constructor()

    fun setFirstName(firstName: String) {
        this.firstName = firstName
    }

    fun setLastName(lastName: String) {
        this.lastName = lastName
    }

    fun setEmail(email: String) {
        this.email = email
    }

    fun setAge(age: Int) {
        this.age = age
    }

    fun setGender(gender: String) {
        this.gender = gender
    }

    fun build() : Person {
        return Person(
            firstName!!,
            lastName,
            email,
            age,
            gender
        )
    }
}
💡
Here we used !! operator after firstName which will throw NullPointerException if the value is null. Ideally, this is not a good practice, we will further discuss how we can fix this.

We have successfully implemented the Builder Design Pattern!

For code brevity and better readability let's Move the Builder class inside Person as a static Inner Class and make the constructor of Person class as Private.

The Person class will look something like this.

class Person private constructor(
    val firstName: String,
    val lastName: String? = null, // optional
    val email: String? = null,    // optional
    val age: Int? = null,         // optional
    val gender: String? = null    // optional
) {
    class Builder {
        var firstName: String? = null
        var lastName: String? = null  // optional
        var email: String? = null    // optional
        var age: Int? = null         // optional
        var gender: String? = null

        fun setFirstName(firstName: String) {
            this.firstName = firstName
        }

        fun setLastName(lastName: String) {
            this.lastName = lastName
        }

        fun setEmail(email: String) {
            this.email = email
        }

        fun setAge(age: Int) {
            this.age = age
        }

        fun setGender(gender: String) {
            this.gender = gender
        }

        fun build() : Person {
            return Person(
                firstName!!,
                lastName,
                email,
                age,
                gender
            )
        }
    }
}

Here is how we can use the Builder Pattern in our project.

fun usage() {  
    val personBuilder = Person.Builder()
    personBuilder.setAge(24)
    personBuilder.setFirstName("Gunish")
    personBuilder.setLastName("Jain")
    personBuilder.setGender("Male")
    val person = personBuilder.build()
}

However, we could improve our implementation by chaining all the builder methods to make it easier to implement and also making it more cleaner.

We just need to return the current instance from all the methods in the builder except the build() method.
We could achieve this by either writing return this in the methods or using apply() which is a scope function that comes with Kotlin’s standard Library. I will be using apply() since it looks much cleaner.

class Person private constructor(
    val firstName: String,
    val lastName: String? = null, // optional
    val email: String? = null,    // optional
    val age: Int? = null,         // optional
    val gender: String? = null    // optional
) {
    class Builder {
        var firstName: String? = null
        var lastName: String? = null  // optional
        var email: String? = null    // optional
        var age: Int? = null         // optional
        var gender: String? = null

        fun setFirstName(firstName: String) = apply {
            this.firstName = firstName
        }

        fun setLastName(lastName: String) = apply { 
            this.lastName = lastName
        }

        fun setEmail(email: String) = apply {
            this.email = email
        }

        fun setAge(age: Int) = apply {
            this.age = age
        }

        fun setGender(gender: String) = apply {
            this.gender = gender
        }

        fun build() : Person {
            return Person(
                firstName!!,
                lastName,
                email,
                age,
                gender
            )
        }
    }
}

The implementation will now look something like this:

fun usage() {
    val person = Person.Builder()
        .setFirstName("Gunish")
        .setLastName("Jain")
        .setAge(24)
        .setEmail("gunish.jain@gmail.com")
        .setGender("Male")
        .build()
}

Now this looks much better :)

However a question may arise when we are constructing the object using builder, we can call the build() method anytime which means any user can create an object that may not have all the values for the mandatory properties, so how we will validate our object?

To address this issue, we must add a validation logic in the constructor of the object before assigning the properties of the class.

Here is the change in the Person Class.

class Person private constructor(
     firstName: String?,
     lastName: String?, // optional
     email: String?,    // optional
     age: Int?,         // optional
     gender: String?    // optional
) {
    val firstName: String
    val lastName: String? // optional
    val email: String?    // optional
    val age: Int?         // optional
    val gender: String?    // optional

    init {
        if (firstName.isNullOrBlank())
            throw IllegalArgumentException("First name is required")

        this.firstName = firstName
        this.lastName = lastName
        this.email = email
        this.age = age
        this.gender = gender
    }
    class Builder {
        var firstName: String? = null
        var lastName: String? = null  // optional
        var email: String? = null     // optional
        var age: Int? = null          // optional
        var gender: String? = null

        fun setFirstName(firstName: String) = apply {
            this.firstName = firstName
        }

        fun setLastName(lastName: String) = apply {
            this.lastName = lastName
        }

        fun setEmail(email: String) = apply {
            this.email = email
        }

        fun setAge(age: Int) = apply {
            this.age = age
        }

        fun setGender(gender: String) = apply {
            this.gender = gender
        }

        fun build() : Person {
            return Person(
                firstName,
                lastName,
                email,
                age,
                gender
            )
        }
    }
}

Let us Summarize the steps to create a Builder Pattern Class.

  1. Create a private constructor

  2. Specify all the properties in the private constructor

  3. Create an inner class Builder - add all the properties to it

  4. Create Setter functions (don't forget to use apply to return object)

  5. Write the function called build() which will return the class with the properties of the object.

Real-Life Use Cases of Builder Design Pattern

  1. AlertDialogs

  2. Notification Builder

  3. Retrofit

  4. Glide

Pros and Cons

Pros

  • Clean and Readable Code

  • Follows Single Responsibility Principle

  • Flexibility in Object Creation

  • Incremental Construction

Cons

  • Harder to understand for beginners

  • Require lots of steps initially