Blog Infos
Author
Published
Topics
Published
Topics

Photo by Guilherme Cunha on Unsplash

 

Have you wondered how we can construct complex objects in steps and encapsulate the process?

Table of Content
  1. Introduction
  2. Problem Statement
  3. Solution — Builder Design Pattern
  4. Validation of the resulting object
  5. Takeaways
  6. Benefits and Drawbacks
  7. Real-life Examples in Android Application Development
  8. Summary
  9. Conclusion
  10. 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

  1. You must produce all the possible permutations and combinations of the constructors.
  2. It is difficult to manage a lot of constructors.
  3. 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.
  4. It violates the Don’t Repeat Yourself (DRY) software principle as a lot of code is repeated in the constructors.
  5. 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

Job Offers


    Senior Android Developer

    SumUp
    Berlin
    • Full Time
    apply now

    Senior Android Engineer

    Carly Solutions GmbH
    Munich
    • Full Time
    apply now

OUR VIDEO RECOMMENDATION

, ,

Kobweb:Creating websites in Kotlin leveraging Compose HTML

Kobweb is a Kotlin web framework that aims to make web development enjoyable by building on top of Compose HTML and drawing inspiration from Jetpack Compose.
Watch Video

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kobweb

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author o ...

Kobweb:Creating websites in Kotlin leveraging Compose HTML

David Herman
Ex-Googler, author of Kob ...

Jobs

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
}
}
view raw Mutableuser.kt hosted with ❤ by GitHub

In the MutableUseradd 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!!
)
}
}
view raw UserBuilder.kt hosted with ❤ by GitHub

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 NoteWe 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., thisfrom 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!!
)
}
}
}
view raw User.kt hosted with ❤ by GitHub

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
}
}
view raw Education.kt hosted with ❤ by GitHub
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 the User.Builder as the constructor of the User class is private.

Address with Builder

  • The constructor of the Address is public, and the client can create an object of Address either by using the Address’ constructor or by using the Address.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 EducationBuilderis an external builder for the Education, 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
  1. Android Notifications
  2. Material Alert Dialog
Android Notifications

The notifications are built using the builder design pattern.
To build an object of Notificationthe 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 or Contact.Builder or Company.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.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu