Menu
Coddy logo textTech

デコレータパターン

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 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

デコレーターは、たとえば顧客の選択に応じて、プログラムの実行中に組み合わされます。一方、サブクラスでは、コードの記述時に1つの組み合わせが固定されます。3つの追加要素をサブクラスとして実装すると、組み合わせごとにクラスが必要になりますが、デコレーターなら、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 を追加)、および内部のすべての価格を2倍にし、トッピングを2回繰り返す DoubleUp を記述してください。次に、Base から開始し、名前ごとに1つのデコレーターを順番に適用する build(names) を記述してください(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プレイグラウンド