Wzorzec budowniczego
Część sekcji Programowanie obiektowe ścieżki Kotlin w Coddy. Lekcja 48 z 57.
Wzorzec builder tworzy obiekt krok po kroku i sprawdza całość na końcu. Builder zbiera części za pomocą łańcuchowych metod, które zwracają samego buildera, a build() tworzy gotowy obiekt:
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)
}
}Wewnątrz main:
val mail = EmailBuilder().to("ada@x.io").to("bo@x.io").subject("Lunch").build()
println(mail)Wynik:
Email(to=[ada@x.io, bo@x.io], subject=Lunch)apply skraca każdy krok: uruchamia blok, w którym obiekt jest dostępny jako this, i zwraca obiekt, więc cały krok mieści się w jednej linii. check(condition) { message } zgłasza IllegalStateException, odpowiednik require dla stanu obiektu:
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
}
}Wewnątrz main:
val request = RequestBuilder().apply {
url = "/users"
method = "POST"
header("Accept", "json")
}.build()
println(request)Wynik:
POST /users [Accept: json]Funkcja, która przyjmuje wyrażenie lambda z odbiorcą typu Builder.() -> Unit, zmienia builder w mały DSL (język specyficzny dla domeny): wewnątrz nawiasów klamrowych builder jest dostępny jako this, więc wywołujący podaje tylko konfigurację:
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()Wewnątrz main:
val lunch = menu("Lunch") {
item("soup", 450)
item("salad", 600)
}
println(lunch)Wynik:
== Lunch ==
soup 4.50
salad 6.00Nazwane i domyślne argumenty w Kotlinie wystarczają w prostych przypadkach: Pizza(size = 30, cheese = true) określa każdy wybór w miejscu wywołania. Wzorzec buildera sprawdza się, gdy obiekt jest składany w kilku krokach, zbiera listy lub wymaga sprawdzenia pól, zanim będzie można go utworzyć:
data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true) // builder nie jest potrzebny
val mail = EmailBuilder() // builder do tworzenia krok po kroku
.to("ada@x.io")
.to("bo@x.io") // zbiera elementy w listę
.subject("Plan")
.build() // sprawdza wszystko na końcuWyzwanie
ŁatwyNapisz kreator dla dostarczonej klasy danych Email. EmailBuilder ma łańcuchowe kroki: to(address) (można wywołać kilka razy; dodaje odbiorców), subject(text), body(text) i urgent(). build() zwraca obiekt Email lub zgłasza wyjątek IllegalStateException z komunikatem no recipients albo no subject (w tej kolejności sprawdzania). Temat pilnej wiadomości e-mail jest poprzedzony ciągiem [URGENT] .
Dostarczony kod odczytuje polecenia (to ada@x.io, subject Hello, body text, urgent, send), tworzy nowy kreator po każdym wierszu --- i wypisuje utworzoną wiadomość e-mail, To: ada@x.io, bo@x.io | [URGENT] Hello | text lub error: no subject.
Twój kod umieść w plikach EmailBuilder.kt i Email.kt. Main.kt zawiera dostarczony kod wejścia/wyjścia i nie można go edytować.
Spróbuj swoich sił
fun main() {
// Dostarczony kod wejścia/wyjścia: pozostaw go bez zmian
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))
}
}
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Klasy i obiekty
Praca z plikamiDefiniowanie klasKonstruktory główneBloki init i walidacjaKonstruktory dodatkowePodsumowanie — karta biblioteczna4Dziedziczenie
Klasy otwarte i podklasyNadpisywanie składowychsuper i konstruktoryKlasy abstrakcyjnePodsumowanie – katalog pracowników7Klasy specjalne
Klasy danychKlasy wyliczenioweHierarchie zapieczętowaneequals, hashCode, toStringPodsumowanie — śledzenie zamówień10Wzorce projektowe, część 2
Delegowanie za pomocą byWzorzec dekoratoraWzorzec budowniczegoWzorzec stanu2Właściwości i hermetyzacja
Własne akcesoryModyfikatory widocznościPrywatne setterylazy i lateinitPowtórka – konto bankowe5Interfejsy
Deklarowanie interfejsówMetody domyślneWiele interfejsówInterfejsy jako typyPowtórzenie – metody płatności8Typy generyczne
Funkcje generyczneKlasy generyczneOgraniczenia typówKowariancja i kontrawariancja: in i outPodsumowanie — kolejka priorytetowa11Projekt: Zarządzanie biblioteką
Książki i czytelnicyWypożyczanie książekPoćwicz samodzielnie: Edytor online Kotlin