Builder Design Pattern in Kotlin

Abhishek Saxena
ProAndroidDev
Published in
9 min readJun 14, 2023

--

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

The Address

The Contact

The Company

The Education

The usage of the constructor would look like this

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

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.

The usage would look like this, and it is much more readable now.

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.

The usage will be as follows.

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.

Now, Create a new class and name it MutableUser with all the attributes as before, a default constructor, and the setters.

In the MutableUser, add another method createImmutableUser() which returns User.

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().

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.

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

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

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

General Implementation

The general implementation of the Builder Design pattern looks as follows

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.

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

Company with Builder

Contact with Builder

Education and EducationBuilder

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

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 Notification, the client must use the builder provided by the Notification API as the constructor of the NotificationCompat is private and cannot be accessed.

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.

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.

--

--

I am a Mobile Full Stack Developer with 2 years of experience with expertise in Native Android and Spring Boot. https://github.com/5AbhishekSaxena