Menu
CoddyTech

构建器模式

本课属于 Coddy Kotlin 学习旅程的“面向对象编程”单元。第 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 的带接收者的 lambda 的函数,会将构建器变成一种小型 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()                                        // 最后检查所有内容
challenge icon

挑战

简单

为提供的数据类 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),每遇到一行 --- 就启动一个新的构建器,并打印构建好的邮件,即 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))
        }
    }
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Kotlin 在线编辑器