Паттерн строитель
Часть раздела Объектно-ориентированное программирование путешествия по Kotlin на Coddy. Урок 48 из 57.
Паттерн строитель создаёт объект пошагово и проверяет весь результат в конце. Строитель собирает части в цепочечных методах, которые возвращают сам строитель, а build() создаёт готовый объект:
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)
}
}Внутри main:
val mail = EmailBuilder().to("ada@x.io").to("bo@x.io").subject("Lunch").build()
println(mail)Вывод:
Email(to=[ada@x.io, bo@x.io], subject=Lunch)apply сокращает каждый шаг: он выполняет блок с объектом в качестве this и возвращает объект, поэтому шаг занимает одну строку. check(condition) { message } выбрасывает IllegalStateException — аналог require для состояния объекта:
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
}
}Внутри main:
val request = RequestBuilder().apply {
url = "/users"
method = "POST"
header("Accept", "json")
}.build()
println(request)Вывод:
POST /users [Accept: json]Функция, принимающая лямбду с получателем типа Builder.() -> Unit, превращает сборщик в небольшой DSL (предметно-ориентированный язык): внутри фигурных скобок сборщик — это this, поэтому вызывающий код содержит только конфигурацию:
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()Внутри main:
val lunch = menu("Lunch") {
item("soup", 450)
item("salad", 600)
}
println(lunch)Вывод:
== Lunch ==
soup 4.50
salad 6.00Именованные аргументы и аргументы по умолчанию в Kotlin уже охватывают простые случаи: Pizza(size = 30, cheese = true) задаёт каждый выбор непосредственно в месте вызова. Билдер становится полезен, когда объект собирается в несколько этапов, содержит списки или требует проверок взаимосвязей между полями, прежде чем он сможет существовать:
data class Pizza(val size: Int = 26, val cheese: Boolean = true, val olives: Boolean = false)
val simple = Pizza(size = 30, olives = true) // билдер не нужен
val mail = EmailBuilder() // билдер для пошаговой сборки
.to("ada@x.io")
.to("bo@x.io") // собирает список
.subject("Plan")
.build() // проверяет всё в концеЗадание
ЛегкоНапишите builder для предоставленного класса данных Email. EmailBuilder содержит следующие связываемые в цепочку шаги: to(address) (может вызываться несколько раз и добавляет получателей), subject(text), body(text) и urgent(). build() возвращает Email или выбрасывает IllegalStateException с сообщением no recipients или no subject (именно в таком порядке выполняются проверки). Срочное письмо получает [URGENT] перед темой.
Предоставленный код считывает команды (to ada@x.io, subject Hello, body text, urgent, send), запускает новый builder после каждой строки --- и выводит созданное письмо, To: ada@x.io, bo@x.io | [URGENT] Hello | text, или error: no subject.
Ваш код должен находиться в EmailBuilder.kt и Email.kt. В Main.kt находится предоставленный код ввода/вывода, и его нельзя изменять.
Попробуйте сами
fun main() {
// Предоставленный код ввода/вывода: оставьте его как есть
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))
}
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Классы и объекты
Работа с файламиОпределение классовПервичные конструкторыБлоки init и валидацияВторичные конструкторыПовторение — читательский билет4Наследование
Открытые классы и подклассыПереопределение членовsuper и конструкторыАбстрактные классыПовторение — каталог сотрудников7Специальные классы
Классы данныхКлассы перечисленийЗапечатанные иерархииequals, hashCode, toStringПовторение — трекер заказов10Паттерны проектирования. Часть 2
Делегирование с помощью byПаттерн декораторПаттерн строительПаттерн состояние2Свойства и инкапсуляция
Пользовательские аксессорыМодификаторы видимостиПриватные сеттерыlazy и lateinitПовторение — банковский счёт5Интерфейсы
Объявление интерфейсовМетоды по умолчаниюМножественные интерфейсыИнтерфейсы как типыПовторение — способы оплаты8Обобщения
Обобщённые функцииОбобщённые классыОграничения типовВариативность с in и outПовторение — очередь с приоритетом11Проект: Управление библиотекой
Книги и читателиВыдача книг3Объекты и компаньоны
Объявления объектовОбъекты-компаньоныКонстанты с constОбъектные выраженияПовторение — билетная касса6Полиморфизм
Динамическая диспетчеризацияПроверка типов и умные приведения типовwhen с типамиФункции-расширенияПовторение — смотритель зоопарка9Паттерны проектирования. Часть 1
Паттерн «Одиночка»Фабричный паттернПаттерн «Наблюдатель»Паттерн «Стратегия»Потренируйтесь самостоятельно: Песочница Kotlin