Menu
Coddy logo textTech

Pattern Builder

Fa parte della sezione Programmazione orientata agli oggetti del percorso Kotlin di Coddy. Lezione 48 di 57.

Il pattern builder crea un oggetto passo dopo passo e verifica il tutto alla fine. Il builder raccoglie i componenti tramite metodi concatenabili che restituiscono il builder stesso, e build() crea l’oggetto completo:

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

All’interno di 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 abbrevia ogni passaggio: esegue un blocco con l'oggetto come this e restituisce l'oggetto, così un passaggio diventa una sola riga. check(condition) { message } genera IllegalStateException, l'equivalente di require per lo stato dell'oggetto:

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

Dentro main:

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

Output:

POST /users [Accept: json]

Una funzione che accetta una lambda con receiver, di tipo Builder.() -> Unit, trasforma il builder in una piccola DSL (linguaggio specifico di dominio): all'interno delle parentesi graffe, il builder è this, quindi chi la chiama scrive solo la configurazione:

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

All'interno di main:

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

Output:

== Lunch ==
soup 4.50
salad 6.00

Gli argomenti con nome e quelli predefiniti di Kotlin coprono già i casi semplici: Pizza(size = 30, cheese = true) indica ogni scelta nel punto di chiamata. Un builder è utile quando l'oggetto viene assemblato in più passaggi, raccoglie liste o richiede controlli tra i campi prima di poter esistere:

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

val mail = EmailBuilder()                            // un builder per la costruzione passo dopo passo
    .to("ada@x.io")
    .to("bo@x.io")                                  // raccoglie un elenco
    .subject("Plan")
    .build()                                        // controlla tutto alla fine
challenge icon

Sfida

Facile

Scrivi un builder per la data class fornita Email. EmailBuilder ha passaggi concatenabili: to(address) (può essere chiamato più volte e aggiunge destinatari), subject(text), body(text) e urgent(). build() restituisce un Email oppure genera IllegalStateException con no recipients o no subject (in quest'ordine di controlli). Un'email urgente riceve [URGENT] davanti all'oggetto.

Il codice fornito legge i comandi (to ada@x.io, subject Hello, body text, urgent, send), avvia un nuovo builder dopo ogni riga --- e stampa l'email creata, To: ada@x.io, bo@x.io | [URGENT] Hello | text, oppure error: no subject.

Il tuo codice va in EmailBuilder.kt e Email.kt. Main.kt contiene il codice di input/output fornito e non può essere modificato.

Provalo tu

fun main() {
    // Codice di input/output fornito: lascialo così com'è
    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 iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Programmazione orientata agli oggetti

Esercitati da solo: Playground Kotlin