Menu
Coddy logo textTech

תבנית Decorator

חלק מהיחידה תכנות מונחה עצמים במסלול ה-Kotlin של Coddy. שיעור 47 מתוך 57.

תבנית הדקורטור מוסיפה התנהגות לאובייקט באמצעות עטיפתו באובייקט אחר בעל אותו ממשק. הדקורטור מעביר קריאות לאובייקט שאותו הוא עוטף ומוסיף את החלק שלו, כך שאפשר לערום דקורטורים:

interface Coffee {
    fun cost(): Int
    fun description(): String
}
class Espresso : Coffee {
    override fun cost() = 200
    override fun description() = "espresso"
}
class Milk(private val inner: Coffee) : Coffee {
    override fun cost() = inner.cost() + 50
    override fun description() = inner.description() + ", milk"
}
class Syrup(private val inner: Coffee) : Coffee {
    override fun cost() = inner.cost() + 30
    override fun description() = inner.description() + ", syrup"
}

בתוך main:

val order = Syrup(Milk(Milk(Espresso())))
println("${order.description()}: ${order.cost()}")

פלט:

espresso, milk, milk, syrup: 330

כשמעטרים משנים את התוצאה, הסדר שלהם חשוב: כל אחד מהם פועל על מה שהאובייקט שבתוכו החזיר:

interface Text {
    fun render(): String
}
class Word(private val w: String) : Text {
    override fun render() = w
}
class Bracket(private val inner: Text) : Text {
    override fun render() = "[" + inner.render() + "]"
}
class Twice(private val inner: Text) : Text {
    override fun render() = inner.render() + inner.render()
}

בתוך main:

println(Twice(Bracket(Word("hi"))).render())
println(Bracket(Twice(Word("hi"))).render())

פלט:

[hi][hi]
[hihi]

עם by, דקורטור כותב רק את החברים שהוא משנה ומעביר אוטומטית את השאר:

interface Source {
    fun read(): String
    fun name(): String
}
class FileSource(private val content: String) : Source {
    override fun read() = content
    override fun name() = "notes.txt"
}
class Censored(private val inner: Source, private val word: String) : Source by inner {
    override fun read() = inner.read().replace(word, "***")
}

בתוך main:

val src = Censored(FileSource("the secret plan"), "secret")
println("${src.name()}: ${src.read()}")

פלט:

notes.txt: the *** plan

משלבים את הדקורטורים בזמן שהתוכנית רצה, למשל לפי הבחירות של הלקוח, ואילו מחלקת משנה מקבעת שילוב אחד בזמן כתיבת הקוד. שלוש תוספות כמחלקות משנה יצריכו מחלקה לכל שילוב; כדקורטורים, fold משרשר אותן מתוך רשימה:

interface Coffee {
    fun cost(): Int
}
class Espresso : Coffee {
    override fun cost() = 200
}
class Extra(private val inner: Coffee, private val price: Int) : Coffee {
    override fun cost() = inner.cost() + price
}

בתוך main:

val prices = mapOf("milk" to 50, "syrup" to 30, "cream" to 70)
val choices = listOf("milk", "cream", "milk")
val coffee = choices.fold<String, Coffee>(Espresso()) { c, extra -> Extra(c, prices.getValue(extra)) }
println(coffee.cost())

פלט:

370
challenge icon

אתגר

קל

הפיצות נבנות באמצעות מעטרים של הממשק הנתון Pizza (עם price() ו-toppings()). כתבו את Base (‏800, ללא תוספות) ואת המעטרים Cheese (‏+150, מוסיף cheese), Olives (‏+100, מוסיף olives) ו-DoubleUp, שמכפיל את המחיר של כל מה שבתוכו וחוזר על התוספות שלו פעמיים. לאחר מכן כתבו את build(names), שמתחיל מ-Base ועוטף אותו במעטר אחד לכל שם, לפי הסדר (cheese, olives או double).

הקוד הנתון קורא שורות של שמות המופרדים בפסיקים, או plain, בונה כל פיצה ומדפיס 1050: cheese, olives או 800: plain.

הקוד שלכם ייכתב בקבצים Base.kt, Cheese.kt, Olives.kt, DoubleUp.kt, Build.kt ו-Pizza.kt. הקובץ Main.kt מכיל את קוד הקלט/פלט הנתון ואי אפשר לערוך אותו.

נסו בעצמכם

fun main() {
    // קוד הקלט/פלט שסופק: השאירו אותו כפי שהוא
    val input = generateSequence(::readLine).toList()
    for (line in input) {
        val names = if (line == "plain") listOf() else line.split(",")
        val pizza = build(names)
        val toppings = pizza.toppings()
        println("${pizza.price()}: " + if (toppings.isEmpty()) "plain" else toppings.joinToString(", "))
    }
}
quiz iconבחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: Kotlin אונליין