1. Introduction
In this quick article, we’ll see how to implement the Builder Design Pattern in Kotlin.
2. Builder Pattern
The Builder pattern is the one that people often use but rarely create on their own.
It’s great to handle the building of objects that may contain a lot of parameters and when we want to make the object immutable once we’re done constructing it.
To learn more, have a look at our tutorial on Creational Design Patterns here.
3. Implementation
Kotlin provides many useful features such as named and default parameters, apply() and data class which avoiding the use of classical Builder pattern implementation.
For that reason, we’ll see first a classical Java-style implementation and then a more Kotlin style short form.
3.1. Java-Style Implementation
Let’s start creating one class – FoodOrder – which contains read-only fields since we don’t want outer objects to access them directly:
class FoodOrder private constructor(builder: FoodOrder.Builder) {
val bread: String = "Flat bread"
val condiments: String?
val meat: String?
val fish: String?
init {
this.bread = builder.bread
this.condiments = builder.condiments
this.meat = builder.meat
this.fish = builder.fish
}
class Builder {
// builder code
}
}
Notice that the constructor is private so that only the nested Builder class can access it.
Let’s now move on to creating the nested class which will be used to build objects:
class Builder {
var bread: String? = null
private set
var condiments: String? = null
private set
var meat: String? = null
private set
var fish: String? = null
private set
fun bread(bread: String) = apply { this.bread = bread }
fun condiments(condiments: String) = apply { this.condiments = condiments }
fun meat(meat: String) = apply { this.meat = meat }
fun fish(fish: String) = apply { this.fish = fish }
fun build() = FoodOrder(this)
}
As we see, our Builder has the same fields as the outer class. For each outer field, we have a matching setter method.
In case we have one or more mandatory fields, instead of using setter methods, let’s make a constructor set them.
Note that we’re using the apply function in order to support the fluent design approach.
Finally, with the build method, we call the FoodOrder constructor.
3.2. Kotlin-Style Implementation
In order to take full advantage of Kotlin, we have to revisit some best practices we got used to in Java. Many of them can be replaced with better alternatives.
Let’s see how we can write idiomatic Kotlin code:
class FoodOrder private constructor(
val bread: String = "Flat bread",
val condiments: String?,
val meat: String?,
val fish: String?) {
data class Builder(
var bread: String? = null,
var condiments: String? = null,
var meat: String? = null,
var fish: String? = null) {
fun bread(bread: String) = apply { this.bread = bread }
fun condiments(condiments: String) = apply { this.condiments = condiments }
fun meat(meat: String) = apply { this.meat = meat }
fun fish(fish: String) = apply { this.fish = fish }
fun build() = FoodOrder(bread, condiments, meat, fish)
fun randomBuild() = bread(bread ?: "dry")
.condiments(condiments ?: "pepper")
.meat(meat ?: "beef")
.fish(fish?: "Tilapia")
.build()
}
}
Kotlin comes with named and default parameters that help to minimize the number of overloads and improve the readability of the function invocation.
We can also take advantage of Kotlin’s data class structure we explore more in another tutorial here.
Finally, as well as in Java-style implementation, apply() is useful for implementing fluent setters.
4. Usages Example
Briefly, let’s have a look at how to build FoodOrder objects using these Builder pattern implementations:
val foodOrder = FoodOrder.Builder()
.bread("white bread")
.meat("bacon")
.condiments("olive oil")
.build()
5. Should We Use the Builder Pattern with Kotlin
Although the Builder pattern is very popular, some aspects of this pattern make it an anti-pattern when applied to Kotlin. This is because using Kotlin features, in many cases, will reward us with safer code, less prone to errors, and minimal boilerplate code.
Let’s dive into some of the problems caused when using this pattern with Kotlin.
5.1. NullPointerException
From our FoodOrder class above, it is possible to create a NullPointerException when implementing the builder pattern. This is because the compiler allows us to omit one of the parameters:
val foodOrder = FoodOrder.Builder()
.bread("white bread")
.meat("bacon")
.condiments("olive oil")
.build()
We omit the fish() method in the build, and could be problematic in a scenario where we extend this class with another attribute. At this point, the compiler won’t be able to indicate to us where we need to adjust the creation of this object and, as such, run into a NullPointerException at runtime. This can be solved by applying the robust builder pattern, but it would entail more boilerplate code.
5.2. Undiscoverable Behavior
Let’s consider the code below. Without looking at the implementation code, let’s try to tell what is created here:
val order = FoodOrder.Builder()
.fish("Sole")
.randomBuild()
From this code, we might expect that we create an order with the fish type “Sole” and random values for the other attributes. But then, we might also think that the builder will provide a random build for all attributes. The only way we can figure out which is the case is by looking into the implementation code for FoodOrder. That will impose extra effort on the calling client and should be avoided.
5.3. Omitted Default Values
Looking at the constructor of FoodOrder, we have a default value for bread as “Flat bread”. However, when using the builder pattern, we reassign the bread value. Otherwise, we will run into a NullPointerException as described above.
5.4. Non-private Constructor
Using the constructor of a class is a natural way to initialize the attributes of that class when creating an object. So instead of using the builder pattern, we could use the constructor method to provide values to attributes. This may however lead to inconsistent objects if all the validation is done in the builder.
5.5. Boilerplate
As we can see, we need to write a lot of boilerplate code for each attribute of our class implementing the builder pattern.
5.6. Solution
As mentioned earlier, Kotlin’s features can address the above problems more efficiently.
Basically, we can get rid of the builder pattern like so:
data class FoodOrder(
var bread: String? = "Flat bread",
var condiments: String? = null,
var meat: String? = null,
var fish: String? = null
)
We now can create objects of this class and initialize its attributes via the constructor:
val order = FoodOrder("Flat bread", "Pepper", "Beef", "Tilapia")
One downside of using this method to create an object is that we must maintain the exact order of parameters in the constructor when providing their values. This is one thing that the builder pattern handles better, as we can provide attribute values in any order.
Because of the issue imposed by using the constructor, Kotlin provides a means to use named parameters:
val order = FoodOrder(
bread = "Flat bread",
condiments = "Pepper",
meat = "Beef",
fish = "Tilapia"
)
This way, we can supply parameters in any order while using their names. It also helps us map a certain value to an attribute parameter without looking into the class.
Furthermore, we can also omit constructor parameters with default values while creating an object:
val order = FoodOrder(
condiments = "Pepper",
meat = "Beef",
fish = "Tilapia"
)
We notice that so far in our current implementation, we are behind on one feature rendered by the builder pattern implementation in the FoodOrder Class: The random build. To solve this using Kotlin’s feature, we can implement the factory method as a companion object:
data class FoodOrder(
var bread: String? = "Flat bread",
var condiments: String? = null,
var meat: String? = null,
var fish: String? = null) {
companion object {
fun random() = FoodOrder(condiments = "Pepper", meat = "Beef", fish = "Tilapia")
}
}
We can now perform a random build by calling the FoodOrder.random() method.
Another important thing we might want to implement is validation. This can be done inside an init block:
data class FoodOrder(
var bread: String? = "Flat bread",
var condiments: String? = null,
var meat: String? = null,
var fish: String? = null) {
init {
meat?.let { require(it.isBlank()){"Meat cannot be blank or omitted"} }
}
}
Now, we are able to achieve the same functionalities we had with the builder pattern just by using Kotlin’s features.
6. Conclusion
The Builder Pattern solves a very common problem in object-oriented programming of how to flexibly create an immutable object without writing many constructors.
When considering a builder, we should focus on whether or not the construction is complex. If we have too simple construction patterns, the effort to create our flexible builder object may far exceed the benefit.
Lastly, we compare the builder pattern’s benefits to Kotlin’s features. We see a couple of problems imposed by the builder pattern and how we can address them using Kotlin’s features.
As always, the code is available on Github.