Menu
Coddy logo textTech

Builder Pattern

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 48 of 57.

The builder pattern creates an object step by step and checks the whole thing at the end. The builder collects the parts in chainable methods that return the builder itself, and build() creates the finished object:

data class Email(val to: List<String>, val subject: String)

class EmailBuilder {
    private val recipients = mutableListOf<String>()
    private var subject = ""

    fun to(address: String): EmailBuilder {
        recipients.add(address)
        return this
    }
    fun subject(text: String): EmailBuilder {
        subject = text
        return this
    }
    fun build(): Email {
        check(recipients.isNotEmpty()) { "no recipients" }
        return Email(recipients.toList(), subject)
    }
}

Inside main:

val mail = EmailBuilder().to("ada@x.io").to("bo@x.io").subject("Lunch").build()
println(mail)

Output:

Email(to=[ada@x.io, bo@x.io], subject=Lunch)

apply shortens every step: it runs a block with the object as this and returns the object, so a step becomes one line. check(condition) { message } throws IllegalStateException, the counterpart of require for the object's state:

class RequestBuilder {
    var url = ""
    var method = "GET"
    private val headers = mutableListOf<String>()

    fun header(name: String, value: String) = apply { headers.add("$name: $value") }

    fun build(): String {
        check(url.isNotEmpty()) { "no url" }
        return "$method $url " + headers
    }
}

Inside main:

val request = RequestBuilder().apply {
    url = "/users"
    method = "POST"
    header("Accept", "json")
}.build()
println(request)

Output:

POST /users [Accept: json]

A function that takes a lambda with receiver, of type Builder.() -> Unit, turns the builder into a small DSL (a domain-specific language): inside the braces, the builder is this, so the caller writes only the configuration:

class MenuBuilder(private val title: String) {
    private val items = mutableListOf<String>()

    fun item(name: String, cents: Int) {
        items.add("$name %d.%02d".format(cents / 100, cents % 100))
    }

    fun build() = "== $title ==\n" + items.joinToString("\n")
}

fun menu(title: String, block: MenuBuilder.() -> Unit): String = MenuBuilder(title).apply(block).build()

Inside main:

val lunch = menu("Lunch") {
    item("soup", 450)
    item("salad", 600)
}
println(lunch)

Output:

== Lunch ==
soup 4.50
salad 6.00

Kotlin's named and default arguments already cover simple cases: Pizza(size = 30, cheese = true) names every choice at the call site. A builder pays off when the object is assembled over several steps, collects lists, or needs checks across fields before it may exist:

data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true)        // no builder needed

val mail = EmailBuilder()                            // a builder for step-by-step assembly
    .to("ada@x.io")
    .to("bo@x.io")                                  // collects a list
    .subject("Plan")
    .build()                                        // checks everything at the end
challenge icon

Challenge

Easy

Write a builder for the supplied data class Email. EmailBuilder has chainable steps: to(address) (may be called several times and adds recipients), subject(text), body(text) and urgent(). build() returns an Email or throws IllegalStateException with no recipients or no subject (in that order of checks). An urgent email gets [URGENT] in front of its subject.

The supplied code reads commands (to ada@x.io, subject Hello, body text, urgent, send), starts a new builder after each --- line and prints the built email, To: ada@x.io, bo@x.io | [URGENT] Hello | text, or error: no subject.

Your code goes in EmailBuilder.kt and Email.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    var builder = EmailBuilder()
    for (cmd in input) {
        when {
            cmd == "send" -> {
                try {
                    println(builder.build())
                } catch (e: IllegalStateException) {
                    println("error: ${e.message}")
                }
            }
            cmd == "---" -> builder = EmailBuilder()
            cmd == "urgent" -> builder.urgent()
            cmd.startsWith("to ") -> builder.to(cmd.drop(3))
            cmd.startsWith("subject ") -> builder.subject(cmd.drop(8))
            else -> builder.body(cmd.drop(5))
        }
    }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground