Builder Design Pattern in Kotlin
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
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
- 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.
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 thebuild()
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 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
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.
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
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