Effective Kotlin Item 34: Consider a primary constructor with named optional arguments
When we define an object and specify how it can be created, the most popular option is to use the primary constructor:
Not only are primary constructors very convenient, but in most cases, it is actually a very good practice to build objects using them. It is common that we need to pass arguments that determine the object’s initial state, as illustrated by the following examples. Starting from the most obvious one: data model objects representing data1. For such object, whole state is initialized using constructor and then hold as properties.
QuotationPresenter has more properties than those declared on a primary constructor. In here
nextQuoteId is a property always initialized with the value
-1. This is perfectly fine, especially when the initial state is set up using default values or using primary constructor parameters.
To better understand why the primary constructor is such a good alternative in the majority of cases, we must first consider common Java patterns related to the use of constructors:
- the telescoping constructor pattern
- the builder pattern
We will see what problems they do solve, and what better alternatives Kotlin offers.
Telescoping constructor pattern
The telescoping constructor pattern is nothing more than a set of constructors for different possible sets of arguments:
Well, this code doesn't really make any sense in Kotlin, because instead we can use default arguments:
Default values are not only cleaner and shorter, but their usage is also more powerful than the telescoping constructor. We can specify just
olives, without mentioning other parameters:
We can also add another named argument either before or after olives:
As you can see, default arguments are more powerful than the telescoping constructor because:
- We can set any subset of parameters with default arguments we want.
- We can provide arguments in any order.
- We can explicitly name arguments to make it clear what each value means.
The last reason is quite important. Think of the following object creation:
It is short, but is it clear? I bet that even the person who declared the pizza class won’t remember in which position bacon is, and in which position cheese can be found. Sure, in an IDE we can see an explanation, but what about those who just scan code or read it on Github? When arguments are unclear, we should explicitly say what their names are using named arguments:
As you can see, constructors with default arguments surpass the telescoping constructor pattern. Though there are more popular construction patterns in Java, and one of them is the Builder Pattern.
Named parameters and default arguments are not allowed in Java. This is the main reason why Java developers use the builder pattern. It allows them to:
- name parameters,
- specify parameters in any order,
- have default values.
Here is an example of a builder defined in Kotlin:
With the builder pattern, we can set those parameters as we want, using their names:
As we’ve already mentioned, these two advantages are fully satisfied by Kotlin named and default arguments:
Comparing these two simple usages, you can see the advantages of named parameters over the builder:
It’s shorter — a constructor or factory method with default arguments is much easier to implement than the builder pattern. It is a time-saver both for the developer who implements this code and for those who read it. It is a significant difference because the builder pattern implementation can be time-consuming. Any builder modification is hard as well, for instance, changing a name of a parameter requires not only name change of the function used to set it, but also name of parameter in this function, body of this function, internal field used to keep it, parameter name in the private constructor etc.
It’s cleaner — when you want to see how an object is constructed, all you need is in a single method instead of being spread around a whole builder class. How are objects held? Do they interact? These are questions that are not so easy to answer when we have a big builder.
Offers simpler usage — the primary constructor is a built-in concept. The builder pattern is an artificial concept, and it requires some knowledge about it. For instance, a developer can easily forget to call the
buildfunction (or in other cases
No problems with concurrence — this is a rare problem, but function parameters are always immutable in Kotlin, while properties in most builders are mutable. Therefore, it is harder to implement a thread-safe build function for a builder.
It doesn't mean that we should always use a constructor instead of a builder. Let’s see cases where different advantages of this pattern shine.
Builders can require a set of values for a name (
addRoute), and allows us to aggregate (
To achieve similar behavior with a constructor we would need to introduce special types to hold more data in a single argument:
This notation is generally badly received in the Kotlin community, and we tend to prefer a builder, but we rather prefer a DSL (Domain Specific Language) builder for such cases:
These kinds of DSL builders are generally preferred over classic builder pattern, since they give more flexibility and cleaner notation. It is true that making a DSL is harder. On the other hand making a builder is already hard. If we decide to invest more time to allow a better notation at the cost of a less obvious definition, why not take this one step further. In return, we will have more flexibility and readability. In the next chapter, we are going to talk more about using DSLs for object creation.
Another advantage of the classic builder pattern is that it can be used as a factory. It might be filled partially and passed further, for example a default dialog in our application:
The alternatives I find more suitable Kotlin are: either make a default object and use
copy to customize its properties, or to create this class using a function with optional parameters.
Both options seem to be popular for unit testing. They seem to be a good alternative (maybe even better), so I don't find builder pattern advantageous.
In the end, the classic builder pattern is rarely the best option in Kotlin. It is sometimes chosen:
- to make code consistent with libraries written in other languages that used builder pattern,
- when we design API to be easily used in other languages that do not support default arguments or DSLs.
Except of that, we rather prefer either a primary constructor with default arguments, or an expressive DSL builder.
Creating objects using a primary constructor is the most appropriate approach for the vast majority of objects in our projects. Telescoping constructor patterns should be treated as obsolete in Kotlin. I recommend using default values instead, as they are cleaner, more flexible, and more expressive. The classic builder pattern is rarely reasonable. In simpler cases we can just use a primary constructor with named arguments, and when we need to create more complex object we can define a DSL builder for that.
A data model is not necessarily a data class, and vice versa. The first concept represents classes in our project that represent data, while the latter is a special support for such classes. This special support is a set of functions that we might not need or that we might need for classes that do not serve as our data model.
A presenter is a kind of object used in the Model-View-Presenter (MVP) architecture, that used to be popular in Android, before its successor MVVM become popular.
Dependency injection is a technique in which an object receives other objects that it depends on. By itself, it does not need any library (or framework) like Koin or Dagger, although I find them useful.