Codementor Events

Effective Java in Kotlin, item 2: Consider a builder when faced with many constructor parameters

Published Apr 11, 2018Last updated Oct 08, 2018
Effective Java in Kotlin, item 2: Consider a builder when faced with many constructor parameters

Originally published at blog.kotlin-academy.com by Marcin Moskala

This item had great influence on Java programmers. It is not a rare situation when we deal with different variants of object creation. Great arguments presented in Effective Java made developers use builder instead of telescoping constructor pattern. Although Kotlin changed a lot - it gives us even better possibilities. We will see it soon 😉

This is the second rule from Effective Java edition 2:

Use BUILDERS when faced with many constructors

Let’s explore it.

Reminder from the book

In Java, a common way to define an object with optional constructor parameters is to use telescoping constructor pattern. When we use telescoping constructor pattern, we define a separate constructor for every set or arguments that we can use. Here is an example in Kotlin:

class Dialog constructor(
        val title: String,
        val text: String?,
        val onAccept: (() -> Unit)?
) {
    constructor(title: String, text: String)
        : this(title, text, null)

    constructor(title: String)
        : this(title, "")
}

// Usage
val dialog1 = Dialog("Some title", "Great dialog", { toast("I was clicked") })

val dialog2 = Dialog("Another dialog","I have no buttons")

val dialog3 = Dialog("Dialog with just a title")

Very popular Android example is how we define custom views.

Even though this pattern is popular in JVM world, Effective Java argues that for bigger or more complex objects we should use Builder pattern instead. Builder first collects the parameters in a readable and compact way, and then validate and instantiate the object. Here is an example:

class Dialog private constructor(
        val title: String,
        val text: String?,
        val onAccept: (() -> Unit)?
) {

   class Builder(val title: String) {
        var text: String? = null 
        var onAccept: (() -> Unit)? = null

        fun setText(text: String?): Builder {
            this.text = text
            return this 
        }

        fun setOnAccept(onAccept: (() -> Unit)?): Builder {
            this.onAccept = onAccept
            return this 
        }

        fun build() = Dialog(title, text, onAccept)
    }
}

// Usage
val dialog1 = Dialog.Builder("Some title")
        .setText("Great dialog")
        .setOnAccept { toast("I was clicked") } 
        .build()

val dialog2 = Dialog.Builder("Another dialog")
        .setText("I have no buttons")
        .build()

val dialog3 = Dialog.Builder("Dialog with just a title").build()

Both declarations and usage are bigger then in telescoping constructor pattern, but builder has very important advantages:

  • Parameters are explicit, so we see name of every parameter when we set it.
  • We can set parameters in any order.
  • It is easier to modify because when we need to change some parameter in telescoping constructor pattern then we need to change it in all constructors that allows it.
  • Builder with filled values can be used like a factory.

This traits make builder pattern more explicit, elastic and better for most classes when we need to set optional parameters.

Named optional parameters

My favorite part of the chapter, from the second edition of Effective Java, is following:

The Builder pattern simulates named optional parameters as found in Ada and Python.

Well, in Kotlin we don’t need to simulate named optional parameters because we can use them directly. Optional parameters are better alternative than builders in most cases. Just compare above builder pattern and below named optional parameters. Both declaration and usage is much cleaner, shorter and more expressive:

class Dialog(
        val title: String,
        val text: String? = null,
        val onAccept: (() -> Unit)? = null
)

// Usage
val dialog1 = Dialog(
        title = "Some title",
        text = "Great dialog",
        onAccept = { toast("I was clicked") }
)
val dialog2 = Dialog(
        title = "Another dialog",
        text = "I have no buttons"
)
val dialog3 = Dialog(title = "Dialog with just a title")

Constructor with named optional parameters have most of advantages of a builder pattern:

  • Parameters are explicit, so we set a name of every parameter when we set it.
  • We can set parameters in any order.
  • It is easier to modify (even easier then builder pattern)

Constructor with named optional parameters looks better in this simple example. But what if we need different creation variants for different parameters? Let’s say that we create different dialog types for a different set of arguments. We can easily solve this problem in builder:

interface Dialog {
    fun show()

    class Builder(val title: String) {
        var text: String? = null 
        var onAccept: (() -> Unit)? = null

        fun setText(text: String?): Builder {
            this.text = text
            return this 
        }

        fun setOnAccept(onAccept: (() -> Unit)?): Builder {
            this.onAccept = onAccept
            return this 
        }

        fun build(): Dialog = when {
            text != null && onAccept != null ->
                TitleTextAcceptationDialog(title, text!!, onAccept!!)
            text != null ->
                TitleTextDialog(title, text!!)
            onAccept != null ->
                TitleAcceptationDialog(title, onAccept!!)
            else -> TitleDialog(title)
        }
    }
}

// Usage
val dialog1 = Dialog.Builder("Some title")
        .setText("Great dialog")
        .setOnAccept { toast("I was clicked") } 
        .build()
val dialog2 = Dialog.Builder("Another dialog")
        .setText("I have no buttons")
        .build()
val dialog3 = Dialog.Builder("Dialog with just a title").build()

Can we solve this kind of problems using named optional parameters? Yes, we can have the same effect using different constructors or using a factory method instead! Here is an example solution for above problem:

interface Dialog {
    fun show()
}

fun makeDialog(
    title: String, 
    text: String? = null, 
    onAccept: (() -> Unit)?
): Dialog = when {
    text != null && onAccept != null -> 
        TitleTextAcceptationDialog(title, text, onAccept)
    text != null -> 
        TitleTextDialog(title, text)
    onAccept != null -> 
        TitleAcceptationDialog(title, onAccept)
    else -> 
        TitleDialog(title)
}

// Usage
val dialog1 = makeDialog(
        title = "Some title",
        text = "Great dialog",
        onAccept = { toast("I was clicked") }
)
val dialog2 = makeDialog(
        title = "Another dialog",
        text = "I have no buttons"
)
val dialog3 = makeDialog(title = "Dialog with just a title")

It is our another example, and again we see advantages of named parameters overbuilder:

  • It is shorter — constructor or factory method is much easier to implement than builder pattern. We don’t need there to specify the name of every optional argument 4 times (as the name of the property, method, parameter and constructor). Type doesn’t need to be declared 3 times (in parameter, property and constructor). This is important because when we want to change some parameter name, then we change only single declaration in a factory method, instead of changing 4 names that suppose to be the same.
  • Is is cleaner — when you want to see how an object is constructed, all you need is in a single method instead of being spread around whole builder class. How are objects hold? Do they interact? These are questions that are not so easy to answer when we have the big builder. On the other hand, class creation is usually clear on factory method.
  • No problems with concurrence —this is rare problem, but function parameters are always immutable in Kotlin, while properties in most builders are mutable. Therefore it is harder to implement thread-safe build function for builder.

One advantage of builder pattern is that builder with filled parameters can be used as a factory. Although it is rare usage, so this advantage is minor.

Another argument for builder pattern is that we can fill builder partially and pass it further. This way we can define methods that create partially filled builder what we can still modify (like default dialog for our application). To have similar possibility for constructor or factory method, we would need auto-currying (which is possible in Kotlin, but not without names and default arguments loss). Although this way of object creation is very rare and it is in general nor preferred. If we want to define default dialog for application, we can create it using function and pass all customization elements as optional arguments. Such method would have more control over dialog creation.

A general rule is that in most cases named optional parameters should be preferred over builder pattern. Although this is not the only new alternative to builder pattern that Kotlin gave us. Another, very popular one, is DSL for object construction. Let’s describe it.

DSL for object construction

Let’s say that we need to set listener that have multiple handlers. Classic Java-like approach would be to use object expression:

taskNameView.addTextChangedListener(object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
        // ... 
    }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        // ... 
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        // no-op 
    }
})

This approach is very incontinent and can be easily replaced with much cleaner factory method with named optional parameters:

fun makeTextWatcher(
        afterTextChanged: ((s: Editable?) -> Unit)? = null,
        beforeTextChanged: ((s: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = null,
        onTextChanged: ((s: CharSequence?, start: Int, before: Int, count: Int) -> Unit)? = null
) = object : TextWatcher {
    override fun afterTextChanged(s: Editable?) {
        afterTextChanged?.invoke(s)
    }

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        beforeTextChanged?.invoke(s, start, count, after)
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        onTextChanged?.invoke(s, start, before, count)
    }
}

// Usage
taskNameView.addTextChangedListener(makeTextWatcher(
        afterTextChanged = { s -> 
            // .. 
        },
        beforeTextChanged = { s, start, count, after -> 
            // ... 
        }
))

Note that we could easily improve it and directly define an extension function to TextView:

taskNameView.addTextChangedListener(
        afterTextChanged = { s -> 
            // .. 
        },
        beforeTextChanged = { s, start, count, after -> 
            // ... 
        }
)

Although we can improve this pattern even more and allow following notation:

taskNameView.addTextChangedListener {
    setAfterTextChanged { s -> 
         // .. 
    },
    setBeforeTextChanged { s, start, count, after -> 
        // ... 
    }
}

This is a simple example of DSL. Functions that support this kind of notation can be found on popular Kotlin libraries like Anko or Android-ktx. This is, for instance, how we can define and display an alert dialog in Anko:

alert("Hi, I'm Roy", "Have you tried turning it off and on again?"){
    yesButton { toast("Oh…") }
    noButton {}
}.show()

The problem is that this kind of notation needs a lot of declarations that will support them. This is how we could define above addOnTextChangedListener:

fun TextView.addOnTextChangedListener(
    config: TextWatcherConfiguration.() -> Unit
) {
    val listener = TextWatcherConfiguration().apply { config() }
    addTextChangedListener(listener)
}

class TextWatcherConfiguration : TextWatcher {

private var beforeTextChangedCallback: (BeforeTextChangedFunction)? = null
    private var onTextChangedCallback: (OnTextChangedFunction)? = null
    private var afterTextChangedCallback: (AfterTextChangedFunction)? = null

fun beforeTextChanged(callback: BeforeTextChangedFunction) {
        beforeTextChangedCallback = callback
    }

fun onTextChanged(callback: OnTextChangedFunction) {
        onTextChangedCallback = callback
    }

fun afterTextChanged(callback: AfterTextChangedFunction) {
        afterTextChangedCallback = callback
    }

override fun beforeTextChanged(
        s: CharSequence, 
        start: Int, 
        count: Int, 
        after: Int
    ) {
        beforeTextChangedCallback?.invoke(s.toString(), start, count, after)
    }

override fun onTextChanged(
         s: CharSequence, 
         start: Int, 
         before: Int, 
         count: Int
    ) {
        onTextChangedCallback?.invoke(s.toString(), start, before, count)
    }

override fun afterTextChanged(s: Editable) {
        afterTextChangedCallback?.invoke(s)
    }
}

private typealias BeforeTextChangedFunction = 
    (text: String, start: Int, count: Int, after: Int) -> Unit
private typealias OnTextChangedFunction = 
    (text: String, start: Int, before: Int, count: Int) -> Unit
private typealias AfterTextChangedFunction = 
    (s: Editable) -> Unit

It is not reasonable to make such declarations for single or even two usages. On the other hand, it is not a problem when we make a library. This is a reason why in most cases we use DSLs defined in libraries. When we have them defined, they are very powerful. Note that inside DSL, we can include control structures (if, for etc.), define variables etc. This is an example of usage of DSL for HTML generation for Kotlin Academy portal:

private fun RDOMBuilder<DIV>.authorDiv(
    author: String?, 
    authorUrl: String?
) {
    author ?: return 
    div(classes = "main-text multiline space-top") { 
        +"Author: " 
        if (authorUrl.isNullOrBlank()) {
            +author
        } else {
            a(href = authorUrl) { +author } 
        }
    }
}

Except declarations, we placed there logic that specifies how this element should be defined. This way DSL is in general far more powerful then constructor or factory method with named optional parameters. It is also more complicated and much harder to define.

Simple DSL when we already have builder

Interesting solution can be observed in some Android projects, where developers implement simplified DSL that reuses excising builder.

Let’s say that we use dialog from library (or framework) that provides builder as a creation method (let’s say it is implemented in Java):

val dialog1 = Dialog.Builder("Some title")
        .setText("Great dialog")
        .setOnAccept { toast("I was clicked") } 
        .build()

This is how we can implement and use very simple DSL builder:

fun Dialog(title: String, init: Dialog.Builder.()->Unit) = 
    Dialog.Builder(title).apply(init).build()

// Usageval dialog1 = Dialog("Some title") {
     text = "Great dialog"
     setOnAccept { toast("I was clicked") }
}

(We can set text like a property only if this method is defined in Java.)

This way we have most advantages of DSL and very simple declaration. This also shows how much in common DSL and builder pattern have. They have similar philosophy, but DSL is like a next level of builder pattern.

Summary

Arguments from Effective Java are still valid in Kotlin, and builder pattern is more reasonable then previous Java alternatives. Albeit Kotlin introduced specifying parameters by names and providing default arguments. Thanks to that, we have better alternative to builder pattern. Kotlin also gave features that allows DSL usage. Well defined DSL is even better alternative because it gives more flexibility and allows logic inside object definition.

Defining how complex objects should be created is not a simple problem and it often requires experience. Although possibilities that Kotlin gives us are very important and they positively influence Kotlin development.


I would like to thank Ilya Ryzhenkov for corrections and important suggestions.

If you need help with learning Kotlin, remember that I give consultations.

To be up-to-date with great news on Kotlin Academy, subscribe to the newsletter and observe Twitter.

To reference me on Twitter, use @MarcinMoskala.

Discover and read more posts from Kotlin Academy
get started
post commentsBe the first to share your opinion
Show more replies