Menu
Coddy logo textTech

ビルダーパターン

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 48/57。

builderパターンは、オブジェクトを段階的に作成し、最後に全体を検証します。builderは、builder自身を返すチェーン可能なメソッドで各部分を集め、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 としてブロックを実行し、オブジェクトを返すため、1つのステップを1行で書けます。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()                                        // 最後にすべてをチェックする
challenge icon

チャレンジ

簡単

提供されたデータクラス Email 用のビルダーを作成してください。EmailBuilder には、チェーン可能なステップとして、to(address)(複数回呼び出すことができ、宛先を追加します)、subject(text)、body(text)、urgent() があります。build() は Email を返すか、no recipients または no subject を伴う IllegalStateException を(このチェック順で)スローします。緊急メールの場合、件名の先頭に [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プレイグラウンド