Menu
Coddy logo textTech

תבנית Builder

חלק מהיחידה תכנות מונחה עצמים במסלול ה-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()                                        // בודק הכול בסוף
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 אונליין