Photo by Guilherme Cunha on Unsplash
Have you wondered how we can construct complex objects in steps and encapsulate the process?
Table of Content
- Introduction
- Problem Statement
- Solution — Builder Design Pattern
- Validation of the resulting object
- Takeaways
- Benefits and Drawbacks
- Real-life Examples in Android Application Development
- Summary
- Conclusion
- References
In this article, we will find out how can we do that and what problems we will solve in the process.
Let us try to understand what Builder Design Pattern is and why we need it.
Builder Design Pattern is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce several types and representations of an object using the same construction code.
Let us take a problem statement to understand the problem and how the builder design pattern solves it.
Problem Statement
We want to create a complex object, i.e., User, with some mandatory and optional properties.
How can we create an object?
- Constructors
– Constructor Overloading
– Telescoping Constructors - Setters
Let us explore the above approaches
Constructors
This is the most common way to instantiate an object and set the initial values or the default values if not provided to an object.
The User
class User { | |
val firstName: String | |
val middleName: String? // optional | |
val lastName: String | |
val address: Address | |
val contact: Contact? // optional | |
val company: Company? // optional | |
val educations: List<Education> | |
constructor( | |
firstName: String, | |
middleName: String?, | |
lastName: String, | |
address: Address, | |
contact: Contact?, | |
company: Company?, | |
educations: List<Education> | |
) { | |
this.firstName = firstName | |
this.middleName = middleName | |
this.lastName = lastName | |
this.address = address | |
this.contact = contact | |
this.company = company | |
this.educations = educations | |
} | |
} |
The Address
class Address( | |
val line1: String, | |
val city: String, | |
val state: String, | |
val country: String, | |
val pinCode: Int, | |
var line2: String? = null, | |
) |
The Contact
class Contact( | |
val twitterHandle: String = "", | |
val githubHandle: String = "", | |
val phoneNumber: String = "", | |
val email: String = "", | |
) |
The Company
class Company( | |
val name: String | |
) |
The Education
class Education( | |
val school: String, | |
val yearOfPassing: Int, | |
val degree: String? = null // optional | |
) |
The usage of the constructor would look like this
fun main() { | |
val address = getAddress() | |
val contact = getContact() | |
val company = getCompany() | |
val educations = getEducations() | |
val user = User( | |
"Abhishek", null, "Saxena", | |
address, contact, | |
company, educations, | |
) | |
val user1 = User( | |
"Abhishek", null, "Saxena", | |
address, null, | |
null, educations, | |
) | |
val user2 = User( | |
"Abhishek", null, "Saxena", | |
address, null, | |
company, educations, | |
) | |
} |
From the above example we can conclude that it is hard to interpret what value is passed for which parameter as the number of parameters in the constructor increases, the readability decreases.
We can solve this issue with constructor overloading.
Constructor Overloading
class User { | |
private val firstName: String | |
private val lastName: String | |
private val address: Address | |
private val educations: List<Education> | |
constructor( | |
firstName: String, | |
lastName: String, | |
address: Address, | |
educations: List<Education> | |
) { | |
this.firstName = firstName | |
this.lastName = lastName | |
this.address = address | |
this.educations = educations | |
} | |
constructor( | |
firstName: String, | |
middleName: String, | |
lastName: String, | |
address: Address, | |
educations: List<Education> | |
) { | |
this.firstName = firstName | |
this.middleName = middleName | |
this.lastName = lastName | |
this.address = address | |
this.educations = educations | |
} | |
constructor( | |
firstName: String, | |
middleName: String, | |
lastName: String, | |
address: Address, | |
company: Company?, | |
educations: List<Education> | |
) { | |
this.firstName = firstName | |
this.middleName = middleName | |
this.lastName = lastName | |
this.address = address | |
this.company = company | |
this.educations = educations | |
} | |
constructor( | |
firstName: String, | |
lastName: String, | |
address: Address, | |
contact: Contact?, | |
educations: List<Education> | |
) { | |
this.firstName = firstName | |
this.lastName = lastName | |
this.address = address | |
this.contact = contact | |
this.educations = educations | |
} | |
} |
The users will be created with the overloaded constructor solves the readability issue to some extent but depends a lot on the parameters of the constructor.
Please Note: If all the parameters are of the same type, then it is difficult to overload the constructor as the compiler will not be able to differentiate between the different parameters of the same type.
There are issues with the overloaded constructors
- You must produce all the possible permutations and combinations of the constructors.
- It is difficult to manage a lot of constructors.
- It is difficult to add or remove parameters from the constructors as it would affect a lot of them at once which can lead to compilation errors.
- It violates the Don’t Repeat Yourself (DRY) software principle as a lot of code is repeated in the constructors.
- Users can get overwhelmed with so many constructors and can get confused about which one to use.
You can resolve the 4th issue using telescoping constructors.
Telescoping Constructors
You can refactor the code as follows and make use of existing constructors.
class User { | |
private val firstName: String | |
private val lastName: String | |
private val address: Address | |
private val educations: List<Education> | |
constructor( | |
firstName: String, | |
lastName: String, | |
address: Address, | |
educations: List<Education> | |
) { | |
this.firstName = firstName | |
this.lastName = lastName | |
this.address = address | |
this.educations = educations | |
} | |
constructor( | |
firstName: String, | |
middleName: String, | |
lastName: String, | |
address: Address, | |
educations: List<Education> | |
) : this(firstName, lastName, address, educations) { | |
this.middleName = middleName | |
} | |
constructor( | |
firstName: String, | |
lastName: String, | |
address: Address, | |
company: Company?, | |
educations: List<Education> | |
) : this(firstName, lastName, address, educations) { | |
this.company = company | |
} | |
constructor( | |
firstName: String, | |
lastName: String, | |
address: Address, | |
contact: Contact?, | |
educations: List<Education> | |
) : this(firstName, lastName, address, educations) { | |
this.contact = contact | |
} | |
constructor( | |
firstName: String, | |
middleName: String, | |
lastName: String, | |
address: Address, | |
contact: Contact, | |
company: Company, | |
educations: List<Education>, | |
) : this(firstName, middleName, lastName, address, educations) { | |
this.contact = contact | |
this.company = company | |
} | |
} |
The usage would look like this, and it is much more readable now.
fun main() { | |
val address = getAddress() | |
val contact = getContact() | |
val company = getCompany() | |
val educations = getEducations() | |
val user = User( | |
"Abhishek", "Saxena", | |
address, educations | |
) | |
val user1 = User( | |
"Abhishek", "Saxena", | |
address, contact, | |
educations, | |
) | |
val user2 = User( | |
"Abhishek", "Saxena", | |
address, | |
company, educations, | |
) | |
} |
Job Offers
It is much easier to understand what is passed for each parameter now because of the overloaded constructors.
Although the telescoping constructor did solve the 4th issue from the constructor overloading approach but did not solve the other issues as they still exist.
Benefits
- Immutable object.
Drawbacks
- Too many constructors
- Need constructors for all the possible permutations and combinations of mandatory and optional attributes.
- Clients can be overwhelmed by so many constructors.
- Hard to maintain.
Let us see another approach to creating objects which is using setters.
Setters
As the name suggests, we will be using setters to set the value of the attributes of the User.
For this approach, you can have all the constructors or just have the default constructor and set all the values using the setter.
To keep the example simple, I will just have the default constructor and set all the values using the setters.
The User would look like this with the setters and the default constructor as follows.
class User { | |
var firstName: String? = null | |
var middleName: String? = null // optional | |
var lastName: String? = null | |
var address: Address? = null | |
var contact: Contact? = null // optional | |
var company: Company? = null // optional | |
var educations: List<Education>? = null | |
constructor() | |
fun setFirstName(firstName: String) { | |
this.firstName = firstName | |
} | |
fun setMiddleName(middleName: String) { | |
this.middleName = middleName | |
} | |
fun setLastName(lastName: String) { | |
this.lastName = lastName | |
} | |
fun setAddress(address: Address) { | |
this.address = address | |
} | |
fun setContact(contact: Contact) { | |
this.contact = contact | |
} | |
fun setCompany(company: Company) { | |
this.company = company | |
} | |
fun setEducations(educations: List<Education>) { | |
this.educations = educations | |
} | |
} |
The usage will be as follows.
fun main() { | |
val address = getAddress() | |
val contact = getContact() | |
val company = getCompany() | |
val educations = getEducations() | |
val user = User() | |
user.setFirstName("Abhishek") | |
user.setLastName("Saxena") | |
user.setAddress(address) | |
user.setEducations(educations) | |
// OR | |
val user1 = User().apply { | |
setFirstName("Abhishek") | |
setLastName("Saxena") | |
setAddress(address) | |
setContact(contact) | |
setEducations(educations) | |
} | |
val user2 = User().apply { | |
setFirstName("Abhishek") | |
setLastName("Saxena") | |
setAddress(address) | |
setCompany(company) | |
} | |
} |
Benefits
- Just one constructor and the values are set using setters.
- The DRY principle is not violated.
- Users can set the values for the attributes they need to set the value for and leave the remaining which will take the default or null value.
Drawbacks
- The User object is mutable.
- The User object may or may not have all mandatory values. This can the fixed by using a constructor with the mandatory values.
- The User object is not validated before it is constructed.
We saw the problem that we are facing with both approaches, either way, we end up with some drawbacks to deal with.
To summarize both approaches we can say that
Approach 1, using Constructors (either of the above-listed approach)
Benefits
- Immutable object
Drawback
- Too many constructors
Approach 2, using setters
Benefits
- Only 1 or 2 constructors
Drawbacks
- Mutable object
Now I hope I have clarified the problem we are facing with the classical approaches to constructing the object.
Let us talk about the solution to the above-listed problems.
Solution — Builder Design Pattern
If you look at the benefits of both the approaches you will see a solution to this problem.
Let me point it out for you.
So, we need a solution using which we should be able to set the values using setters, and the new object should be immutable.
Pretty simple!
Create an immutable User and let’s call it a User
.
class User { | |
val firstName: String | |
val middleName: String // optional | |
val lastName: String | |
val address: Address | |
val contact: Contact // optional | |
val company: Company // optional | |
val educations: List<Education> | |
constructor( | |
firstName: String, | |
middleName: String, | |
lastName: String, | |
address: Address, | |
contact: Contact, | |
company: Company, | |
educations: List<Education> | |
) { | |
this.firstName = firstName | |
this.middleName = middleName | |
this.lastName = lastName | |
this.address = address | |
this.contact = contact | |
this.company = company | |
this.educations = educations | |
} | |
} |
Now, Create a new class and name it MutableUser
with all the attributes as before, a default constructor, and the setters.
class MutableUser { | |
var firstName: String? = null | |
var middleName: String? = null // optional | |
var lastName: String? = null | |
var address: Address? = null | |
var contact: Contact? = null // optional | |
var company: Company? = null // optional | |
var educations: List<Education>? = null | |
constructor() | |
fun setFirstName(firstName: String) { | |
this.firstName = firstName | |
} | |
fun setMiddleName(middleName: String) { | |
this.middleName = middleName | |
} | |
fun setLastName(lastName: String) { | |
this.lastName = lastName | |
} | |
fun setAddress(address: Address) { | |
this.address = address | |
} | |
fun setContact(contact: Contact) { | |
this.contact = contact | |
} | |
fun setCompany(company: Company) { | |
this.company = company | |
} | |
fun setEducations(educations: List<Education>) { | |
this.educations = educations | |
} | |
} |
In the MutableUser
, add another method createImmutableUser()
which returns User
.
fun createImmutableUser(): User { | |
return User( | |
firstName!!, | |
middleName, | |
lastName!!, | |
address!!, | |
contact, | |
company, | |
educations!! | |
) | |
} |
When you are going to add this method then you will see a lot of errors due to nullable values.
Please Note: There is a flaw in the above method, that is, the use of !! operators which will throw NullPointerException if the value is null but do not worry, we will address it later in the how to validate the object section.
If you look closely at the MutableUser
class, it is responsible for creating the User
object in a step-by-step manner and the new User
which is returned is an immutable object.
So, we have solved the problem which is faced in the beginning as we can create an immutable object in a step-by-step approach.
Lastly, rename the MutableUser
class to UserBuilder
and createImmutableUser()
to build()
.
class UserBuilder() { | |
var firstName: String? = null | |
var middleName: String? = null // optional | |
var lastName: String? = null | |
var address: Address? = null | |
var contact: Contact? = null // optional | |
var company: Company? = null // optional | |
var educations: List<Education>? = null | |
fun setFirstName(firstName: String) { | |
this.firstName = firstName | |
} | |
fun setMiddleName(middleName: String) { | |
this.middleName = middleName | |
} | |
fun setLastName(lastName: String) { | |
this.lastName = lastName | |
} | |
fun setAddress(address: Address) { | |
this.address = address | |
} | |
fun setContact(contact: Contact) { | |
this.contact = contact | |
} | |
fun setCompany(company: Company) { | |
this.company = company | |
} | |
fun setEducations(educations: List<Education>) { | |
this.educations = educations | |
} | |
fun build(): User { | |
return User( | |
firstName!!, | |
middleName, | |
lastName!!, | |
address!!, | |
contact, | |
company, | |
educations!! | |
) | |
} | |
} |
We have implemented the Builder Design Pattern.
Lastly, move the UserBuilder
inside User
as a static inner class, rename it to Builder
, and make the constructor of the User
class private.
The User
class would look like this.
class User private constructor( | |
val firstName: String, | |
val middleName: String?, // optional | |
val lastName: String, | |
val address: Address, | |
val contact: Contact?, // optional | |
val company: Company?, // optional | |
val educations: List<Education>, | |
) { | |
class Builder { | |
var firstName: String? = null | |
var middleName: String? = null // optional | |
var lastName: String? = null | |
var address: Address? = null | |
var contact: Contact? = null // optional | |
var company: Company? = null // optional | |
var educations: List<Education>? = null | |
fun setFirstName(firstName: String) { | |
this.firstName = firstName | |
} | |
fun setMiddleName(middleName: String) { | |
this.middleName = middleName | |
} | |
fun setLastName(lastName: String) { | |
this.lastName = lastName | |
} | |
fun setAddress(address: Address) { | |
this.address = address | |
} | |
fun setContact(contact: Contact) { | |
this.contact = contact | |
} | |
fun setCompany(company: Company) { | |
this.company = company | |
} | |
fun setEducations(educations: List<Education>) { | |
this.educations = educations | |
} | |
fun build(): User { | |
return User( | |
firstName!!, | |
middleName, | |
lastName!!, | |
address!!, | |
contact, | |
company, | |
educations!! | |
) | |
} | |
} | |
} |
Please Note: We still have not addressed the use of
!! in the
build()
method which we will discuss later in the validation section.
The usage will be like this
fun main() { | |
val contact = getContact() | |
val userBuilder = User.Builder() | |
userBuilder.setFirstName("Abhishek") | |
userBuilder.setLastName("Saxena") | |
userBuilder.setContact(contact) | |
val user = userBuilder.build() // <- user object is built here | |
} |
You may notice that userBuilder
is shouting at us while we build the object, and we can make a slight change to make this a fluent API (Application Programming Interface) using builder by chaining all the builder methods.
To chain the methods of the builder, they must return the current instance, i.e., this
, from all the methods in the builder except the build()
method.
After refactoring, the User
with Builder will be
class User private constructor( | |
val firstName: String, | |
val middleName: String?, // optional | |
val lastName: String, | |
val address: Address, | |
val contact: Contact?, // optional | |
val company: Company?, // optional | |
val educations: List<Education>, | |
) { | |
class Builder { | |
var firstName: String? = null | |
var middleName: String? = null // optional | |
var lastName: String? = null | |
var address: Address? = null | |
var contact: Contact? = null // optional | |
var company: Company? = null // optional | |
var educations: List<Education>? = null | |
fun setFirstName(firstName: String): Builder { | |
this.firstName = firstName | |
return this | |
} | |
// OR | |
fun setMiddleName(middleName: String) = apply { | |
this.middleName = middleName | |
} | |
fun setLastName(lastName: String) = apply { | |
this.lastName = lastName | |
} | |
fun setAddress(address: Address) = apply { | |
this.address = address | |
} | |
fun setContact(contact: Contact) = apply { | |
this.contact = contact | |
} | |
fun setCompany(company: Company) = apply { | |
this.company = company | |
} | |
fun setEducations(educations: List<Education>) = apply { | |
this.educations = educations | |
} | |
fun build(): User { | |
return User( | |
firstName!!, | |
middleName, | |
lastName!!, | |
address!!, | |
contact, | |
company, | |
educations!! | |
) | |
} | |
} | |
} |
apply() is a scope function that comes with Kotlin’s standard library.
The context object is available as a receiver (this). The return value is the object itself.
The usage will be updated to
fun main() { | |
val contact = getContact() | |
val user = User.Builder() | |
.setFirstName("Abhishek") | |
.setLastName("Saxena") | |
.setContact(contact) | |
.build() // <- user object is built here | |
} |
General Implementation
The general implementation of the Builder Design pattern looks as follows
class User private constructor( | |
firstName: String?, | |
// remaining properties… | |
) { | |
val firstName: String | |
// other properties... | |
init { | |
// validations, if any | |
// set values | |
this.firstName = firstName | |
// remaining properties ... | |
} | |
class Builder { | |
private var firstName: String? = null | |
Other Properties | |
Public Setters for the builder | |
// build() that returns the User object. | |
fun build(): User {...} | |
} | |
} |
Validation of the resulting object
You may be wondering that when we are constructing the object using the builder, we can call the build()
method anytime we like, which means that the client can create an object which may not have values for all the mandatory attributes, hence an invalid object.
To address this issue, we must validate the object before constructing it.
The question is, where should the validation logic be put?
You may think that we should put it inside the build()
method before calling the constructor of the object we are creating, here, User.
This may seem the right choice, but this approach violates the Single Responsibility Principle (SRP) as the build()
method now has multiple responsibilities — validate the properties and create a new object using the validated properties.
So, how can we solve this problem?
We should place the validation logic in the constructor of the object before the values are assigned to the properties of the class, like this.
class User private constructor( | |
firstName: String?, | |
middleName: String?, // optional | |
lastName: String?, | |
address: Address?, | |
contact: Contact?, // optional | |
company: Company?, // optional | |
educations: List<Education>, | |
) { | |
val firstName: String | |
val middleName: String? // optional | |
val lastName: String | |
val address: Address | |
val contact: Contact? // optional | |
val company: Company? // optional | |
val educations: List<Education> | |
init { | |
if (firstName.isNullOrBlank()) | |
throw IllegalArgumentException("First name is required") | |
if (lastName.isNullOrBlank()) | |
throw IllegalArgumentException("Last name is required") | |
if (address == null) | |
throw IllegalArgumentException("Address is required") | |
if (educations.isEmpty()) | |
throw IllegalArgumentException("Education list is required") | |
this.firstName = firstName | |
this.middleName = middleName | |
this.lastName = lastName | |
this.address = address | |
this.contact = contact | |
this.company = company | |
this.educations = educations | |
} | |
class Builder { | |
private var firstName: String? = null | |
private var middleName: String? = null // optional | |
private var lastName: String? = null | |
private var address: Address? = null | |
private var contact: Contact? = null // optional | |
private var company: Company? = null // optional | |
private val educations = mutableListOf<Education>() | |
fun setFirstName(firstName: String): Builder { | |
this.firstName = firstName | |
return this | |
} | |
// OR | |
fun setMiddleName(middleName: String) = apply { | |
this.middleName = middleName | |
} | |
fun setLastName(lastName: String) = apply { | |
this.lastName = lastName | |
} | |
fun setAddress(address: Address) = apply { | |
this.address = address | |
} | |
fun setContact(contact: Contact) = apply { | |
this.contact = contact | |
} | |
fun setCompany(company: Company) = apply { | |
this.company = company | |
} | |
fun addEducation(education: Education) = apply { | |
this.educations.add(education) | |
} | |
fun addEducation(educations: List<Education>) = apply { | |
this.educations.addAll(educations) | |
} | |
fun setEducations(educations: List<Education>) = apply { | |
this.educations.clear() | |
this.educations.addAll(educations) | |
} | |
fun build(): User { | |
return User( | |
firstName, | |
middleName, | |
lastName, | |
address, | |
contact, | |
company, | |
educations | |
) | |
} | |
} | |
} |
Now, we do not need the !! in the
build()
method. Also, this way we can ensure that the new object is a valid object with all the mandatory values.
We can implement the Builder Design Pattern in other classes — Address, Company, Contact, and Education.
Address with Builder
class Address( | |
line1: String?, | |
line2: String?, | |
city: String?, | |
state: String?, | |
country: String?, | |
pinCode: Int? | |
) { | |
val line1: String | |
val line2: String? | |
val city: String | |
val state: String | |
val country: String | |
val pinCode: Int | |
init { | |
if (line1.isNullOrBlank()) | |
throw IllegalArgumentException("Line1 must not be null or blank.") | |
if (city.isNullOrBlank()) | |
throw IllegalArgumentException("City must not be null or blank.") | |
if (state.isNullOrBlank()) | |
throw IllegalArgumentException("State must not be null or blank.") | |
if (country.isNullOrBlank()) | |
throw IllegalArgumentException("Country must not be null or blank.") | |
if (pinCode == null) | |
throw IllegalArgumentException("Pin code must not be null.") | |
this.line1 = line1 | |
this.line2 = line2 | |
this.city = city | |
this.state = state | |
this.country = country | |
this.pinCode = pinCode | |
} | |
class Builder { | |
private var line1: String? = null | |
private var city: String? = null | |
private var state: String? = null | |
private var country: String? = null | |
private var pinCode: Int? = null | |
private var line2: String? = null | |
fun setLine1(line1: String?) = apply { | |
this.line1 = line1 | |
} | |
fun setLine2(line2: String?) = apply { | |
this.line2 = line2 | |
} | |
fun setCity(city: String?) = apply { | |
this.city = city | |
} | |
fun setState(state: String?) = apply { | |
this.state = state | |
} | |
fun setCountry(country: String?) = apply { | |
this.country = country | |
} | |
fun setPinCode(pinCode: Int) = apply { | |
this.pinCode = pinCode | |
} | |
fun build(): Address { | |
return Address( | |
line1 = line1, | |
line2 = line2, | |
city = city, | |
state = state, | |
country = country, | |
pinCode = pinCode | |
) | |
} | |
} | |
} |
Company with Builder
class Company( | |
name: String? | |
) { | |
val name: String | |
init { | |
if (name.isNullOrBlank()) | |
throw IllegalArgumentException("Name must not be null or blank.") | |
this.name = name | |
} | |
class Builder { | |
private var name: String? = null | |
fun setName(name: String) = apply { | |
this.name = name | |
} | |
fun build(): Company { | |
return Company(name) | |
} | |
} | |
} |
Contact with Builder
class Contact( | |
twitterHandle: String, | |
githubHandle: String, | |
phoneNumber: String, | |
email: String, | |
) { | |
val twitterHandle: String | |
val githubHandle: String | |
val phoneNumber: String | |
val email: String | |
init { | |
this.twitterHandle = twitterHandle.ifBlank { | |
throw IllegalArgumentException("Twitter handle must not be blank.") | |
} | |
this.githubHandle = githubHandle.ifBlank { | |
throw IllegalArgumentException("GitHub handle must not be blank.") | |
} | |
this.phoneNumber = phoneNumber.ifBlank { | |
throw IllegalArgumentException("Phone number must not be blank.") | |
} | |
this.email = email.ifBlank { | |
throw IllegalArgumentException("Email must not be blank.") | |
} | |
} | |
class Builder { | |
private var twitterHandle: String = "" | |
private var githubHandle: String = "" | |
private var phoneNumber: String = "" | |
private var email: String = "" | |
fun setTwitterHandle(twitterHandle: String) = apply { | |
this.twitterHandle = twitterHandle | |
} | |
fun setGithubHandle(githubHandle: String) = apply { | |
this.githubHandle = githubHandle | |
} | |
fun setPhoneNumber(phoneNumber: String) = apply { | |
this.phoneNumber = phoneNumber | |
} | |
fun setEmail(email: String) = apply { | |
this.email = email | |
} | |
fun build(): Contact { | |
return Contact( | |
twitterHandle = twitterHandle, | |
githubHandle = githubHandle, | |
phoneNumber = phoneNumber, | |
email = email | |
) | |
} | |
} | |
} |
Education and EducationBuilder
class Education( | |
school: String, | |
yearOfPassing: Int?, | |
degree: String? // optional | |
) { | |
val school: String | |
val yearOfPassing: Int | |
val degree: String? // optional | |
init { | |
if (school.isBlank()) | |
throw IllegalArgumentException("School must not be blank.") | |
if (yearOfPassing == null) { | |
throw IllegalArgumentException("School must not be blank.") | |
} | |
this.school = school | |
this.yearOfPassing = yearOfPassing | |
this.degree = degree | |
} | |
} |
class EducationBuilder { | |
private var school: String = "" | |
private var yearOfPassing: Int? = null | |
private var degree: String? = null // optional | |
fun setSchool(school: String): EducationBuilder = apply { | |
this.school = school | |
} | |
fun setYearOfPassing(yearOfPassing: Int?): EducationBuilder = apply { | |
this.yearOfPassing = yearOfPassing | |
} | |
fun setDegree(degree: String?): EducationBuilder = apply { | |
this.degree = degree | |
} | |
fun build(): Education { | |
return Education(school, yearOfPassing, degree) | |
} | |
} |
Takeaways
Takeaways from different implementations of the Builder pattern in the above examples.
User with Builder
- The default values of the builder attributes are either null or an empty list.
- There are multiple ways to add education
–builder.addEducation(Education)
— adds one education at a time.
–builder.addEducation(List<Education>)
— adds a list of educations.
–builder.setEducations(List<Education>)
— set a list of educations. - It is mandatory to create the
User
object using theUser.Builder
as the constructor of theUser
class is private.
Address with Builder
- The constructor of the
Address
is public, and the client can create an object ofAddress
either by using the Address’ constructor or by using theAddress.Builder
.
Company with Builder
- Same as Address with Builder
Contact with Builder
- The default value of the builder attributes is an empty string.
- Same as Address with Builder
Education and EducationBuilder
- The EducationBuilder is a separate class and is not an inner class of the
Education
like the other builders. - The
EducationBuilder
is an external builder for theEducation
, this approach is useful when you do not own the class, but you still want to build the object using Builder Design Pattern. A common use case would be creating a builder for a third-party library. - The default values of the builder attributes are a mixture of empty string and null values.
Usage
fun main() { | |
val address = getAddress() | |
val company = getCompany() | |
val contact = getContact() | |
val schoolEducation = getSchoolEducation() | |
val universityEducation = getUniversityEducation() | |
val educations = listOf(schoolEducation, universityEducation) | |
val user = User.Builder() | |
.setFirstName("Abhishek") | |
.setLastName("Saxena") | |
.setAddress(address) | |
.setCompany(company) | |
.setContact(contact) | |
.setEducations(educations) // <- a list of education is set | |
.build() // <- user object is built here | |
val user1 = User.Builder() | |
.setFirstName("Abhishek") | |
.setLastName("Saxena") | |
.setAddress(address) | |
.setCompany(company) | |
.addEducation(educations) // <- a list of education is added | |
.build() // <- user object is built here | |
val user2 = User.Builder() | |
.setFirstName("Abhishek") | |
.setLastName("Saxena") | |
.setAddress(address) | |
.addEducation(schoolEducation) | |
.addEducation(universityEducation) // <- Education is added one at a time | |
.build() // <- user object is built here | |
} | |
private fun getAddress(): Address = Address.Builder() | |
.setLine1("test") | |
.setCity("Delhi") | |
.setState("Delhi") | |
.setCountry("India") | |
.setPinCode(123456) | |
.build() | |
private fun getCompany(): Company = Company.Builder() | |
.setName("ABC") | |
.build() | |
private fun getContact(): Contact = Contact.Builder() | |
.setEmail("abc@def.com") | |
.build() | |
private fun getSchoolEducation(): Education = EducationBuilder() | |
.setSchool("ABC School") | |
.setYearOfPassing(2014) | |
.build() | |
private fun getUniversityEducation(): Education = EducationBuilder() | |
.setSchool("ABC University") | |
.setDegree("B.Tech") | |
.setYearOfPassing(2020) | |
.build() |
Benefits and Drawbacks
Benefits
- Encapsulates the way a complex object is constructed.
- Allows objects to be constructed in a multistep and varying process.
- Easy to refactor.
Drawbacks
- It can be a complex pattern to implement.
- It can be hard for clients to discover the pattern.
Real-Life Examples In Android Application Development
- Android Notifications
- Material Alert Dialog
Android Notifications
The notifications are built using the builder design pattern.
To build an object of Notification
, the client must use the builder provided by the Notification API as the constructor of the NotificationCompat
is private and cannot be accessed.
private fun buildSimpleNotification(): Notification { | |
val pendingIntent: PendingIntent = getPendingIntent() | |
val builder = NotificationCompat.Builder(this, CHANNEL_ID) | |
.setSmallIcon(R.drawable.ic_android_black_24dp) | |
.setContentTitle("Sample notification") | |
.setContentText("This is the long content of the notification. Happy coding.") | |
.setStyle( | |
NotificationCompat.BigTextStyle() | |
.bigText("This is much bigger content of the notification. Happy coding!") | |
) | |
.setPriority(NotificationCompat.PRIORITY_HIGH) | |
.setContentIntent(pendingIntent) | |
return builder.build() | |
} |
Material Alert Dialog
The Material Alert Dialogs are built using the MaterialAlertDialogBuilder
which uses the builder design pattern.
The MaterialAlertDialogBuilder is an example of an external builder, just like the EducationBuilder in our example, as it comes from
com.google.android.material.dialog package
, which is part of the material library by Google, but the build method returns AlertDialog
object which is part of androidx.appcompat.app
package which is part of the AndroidX AppCompat library.
private fun buildSimpleDialog(): AlertDialog { | |
return MaterialAlertDialogBuilder(this) | |
.setTitle("Delete Task") | |
.setPositiveButton( | |
"Delete" | |
) { _, _ -> Toast.makeText(this, "Delete clicked", Toast.LENGTH_SHORT).show() } | |
.setNegativeButton( | |
"Cancel" | |
) { _, _ -> Toast.makeText(this, "Cancel clicked", Toast.LENGTH_SHORT).show() } | |
.setCancelable(true) | |
.setMessage("Are you sure that you would like to delete the task?") | |
.setIcon(R.drawable.ic_baseline_delete_24) | |
.create() | |
} | |
Summary
- Use the Builder pattern when you must build a complex object.
- Using Builder pattern to build objects in steps.
- You can force the client to use the Builder to build the object by making the constructor(s) private (check
User.Builder
). - The Builder can act as an added API for building the objects (check
Address.Builder
orContact.Builder
orCompany.Builder
). - You can have the Builder out of the Product as an external Builder (check
EducationBuilder
). - Use
build()
method to return the Product.
Conclusion
The Builder Design Pattern is an extremely useful pattern when the client must create complex objects as it allows the client to construct objects in steps. The object is still in the mediator or builder state until it is finally built and returned to the client.
The builder pattern has a diverse number of variations as shown in the different examples above, but the structure of the builder class and the intent of the pattern stays the same. The builder design pattern also gives a lot of flexibility while the object is being created by the client as it can set the values in multiple ways, for example — education in User.Builder
.
How do you solve a similar problem in your project? Comment below or reach out to me on Twitter or LinkedIn.
Thank you very much for reading the article. Don’t forget to 👏 if you liked it.
References
- Head First Design Pattern by Eric Freeman
- https://www.youtube.com/watch?v=6Wi2XZeAf-Q
- https://www.youtube.com/watch?v=4ff_KZdvJn8
- https://www.youtube.com/watch?v=dpXlh-Bxk6I
- https://refactoring.guru/design-patterns/builder/java/example#example-0–director-Director-java
— — Abhishek Saxena
This article was previously published on proandroiddev.com