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:
We must write all the possible permutations and combinations of the constructors.
It violates the Don’t Repeat Yourself (DRY) software principle as a lot of code is repeated in the constructors.
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.
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
)
}
}
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.
Create a private constructor
Specify all the properties in the private constructor
Create an inner class Builder - add all the properties to it
Create Setter functions (don't forget to use apply to return object)
Write the function called build() which will return the class with the properties of the object.
Real-Life Use Cases of Builder Design Pattern
AlertDialogs
Notification Builder
Retrofit
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