Menu
Coddy logo textTech

Builder-Muster

Teil des Abschnitts Objektorientierte Programmierung der Kotlin-Journey von Coddy. Lektion 48 von 57.

Das Builder-Muster erstellt ein Objekt Schritt für Schritt und überprüft das gesamte Objekt am Ende. Der Builder sammelt die Bestandteile in verkettbaren Methoden, die den Builder selbst zurückgeben, und build() erstellt das fertige Objekt:

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)
    }
}

Innerhalb von main:

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

Ausgabe:

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

apply verkürzt jeden Schritt: Es führt einen Block mit dem Objekt als this aus und gibt das Objekt zurück, sodass ein Schritt zu einer Zeile wird. check(condition) { message } löst IllegalStateException aus, das Gegenstück zu require für den Zustand des Objekts:

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
    }
}

Innerhalb von main:

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

Ausgabe:

POST /users [Accept: json]

Eine Funktion, die ein Lambda mit Empfänger vom Typ Builder.() -> Unit entgegennimmt, verwandelt den Builder in eine kleine DSL (domänenspezifische Sprache): Innerhalb der geschweiften Klammern ist der Builder this, sodass der Aufrufer nur die Konfiguration schreibt:

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()

Innerhalb von main:

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

Ausgabe:

== Lunch ==
soup 4.50
salad 6.00

Kotl​ins benannte und Standardargumente decken bereits einfache Fälle ab: Pizza(size = 30, cheese = true) benennt jede Auswahl am Aufrufort. Ein Builder lohnt sich, wenn das Objekt über mehrere Schritte zusammengesetzt wird, Listen sammelt oder vor seiner Erstellung Prüfungen über mehrere Felder hinweg benötigt:

data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true)        // kein Builder nötig

val mail = EmailBuilder()                            // ein Builder für schrittweisen Aufbau
    .to("ada@x.io")
    .to("bo@x.io")                                  // sammelt eine Liste
    .subject("Plan")
    .build()                                        // prüft alles am Ende
challenge icon

Aufgabe

Einfach

Schreibe einen Builder für die bereitgestellte Datenklasse Email. EmailBuilder verfügt über verkettbare Schritte: to(address) (kann mehrmals aufgerufen werden und fügt Empfänger hinzu), subject(text), body(text) und urgent(). build() gibt ein Email zurück oder löst IllegalStateException mit no recipients oder no subject aus (in dieser Reihenfolge der Prüfungen). Eine dringende E-Mail erhält [URGENT] vor ihrem Betreff.

Der bereitgestellte Code liest Befehle (to ada@x.io, subject Hello, body text, urgent, send), startet nach jeder Zeile mit --- einen neuen Builder und gibt die erstellte E-Mail, To: ada@x.io, bo@x.io | [URGENT] Hello | text, oder error: no subject aus.

Dein Code gehört in EmailBuilder.kt und Email.kt. Main.kt enthält den bereitgestellten Ein-/Ausgabecode und kann nicht bearbeitet werden.

Probier es selbst

fun main() {
    // Bereitgestellter Ein-/Ausgabecode: belasse ihn unverändert
    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 iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Kotlin-Playground