Menu
Coddy logo textTech

デコレータパターン

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

デコレータパターンは、同じプロトコルを持つ別のオブジェクトでオブジェクトをラップすることにより、そのオブジェクトに振る舞いを追加します。ラッパーはラップされたオブジェクトに呼び出しを転送し、その前後に自身の処理を追加します。

protocol Coffee {
    func cost() -> Int
    func label() -> String
}

struct Espresso: Coffee {
    func cost() -> Int { 200 }
    func label() -> String { "espresso" }
}

struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}

let drink = WithMilk(base: Espresso())
print(drink.label())
print(drink.cost())

出力:

espresso + milk
250

デコレーター自体がCoffeeであるため、組み合わせごとにサブクラスを用意しなくても、デコレーター同士を任意の組み合わせや順序でラップできます。

protocol Coffee { func cost() -> Int; func label() -> String }
struct Espresso: Coffee { func cost() -> Int { 200 }; func label() -> String { "espresso" } }
struct WithMilk: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 50 }
    func label() -> String { base.label() + " + milk" }
}
struct WithSyrup: Coffee {
    let base: any Coffee
    func cost() -> Int { base.cost() + 30 }
    func label() -> String { base.label() + " + syrup" }
}

let fancy = WithSyrup(base: WithMilk(base: WithMilk(base: Espresso())))
print(fancy.label())
print(fancy.cost())

出力:

espresso + milk + milk + syrup
330

デコレータは、変更されない既存のコンポーネントの周囲に、ロギング、キャッシュ、バリデーションなどの横断的な機能を追加するのに適しています。

protocol DataSource { func fetch(_ id: Int) -> String }
struct Database: DataSource { func fetch(_ id: Int) -> String { "row \(id)" } }

struct Logged: DataSource {
    let inner: any DataSource
    func fetch(_ id: Int) -> String {
        print("fetching \(id)")
        return inner.fetch(id)
    }
}

let source: any DataSource = Logged(inner: Database())
print(source.fetch(7))

出力:

fetching 7
row 7

オブジェクトを使用するコードから見えるのはプロトコルだけなので、装飾されたオブジェクトと通常のオブジェクトを見分けることはできません。これが、このパターンを後から安全に追加できる理由です。

func printReceipt(_ c: any Coffee) {      // Espresso および任意のラップされたバージョンで動作する
    print("\(c.label()): \(c.cost())")
}
challenge icon

チャレンジ

簡単

ピザはデコレータを使って構築されます。プロトコル Pizza には、price() と toppings()(名前の配列)があります。Base はトッピングなしで 800 です。デコレータ Cheese(+150)、Olives(+100)、Double(内部のすべての価格を2倍にし、トッピングを2回繰り返す)を実装してください。Base から開始し、名前ごとに順番どおり1つのデコレータでラップする build(_:) を完成させてください。

提供されたコードは、カンマ区切りのトッピング名の行(または空行)を読み込み、それぞれのピザを構築して、1050: cheese, olives または 800: plain を出力します。

コードは Pizza.swift、Base.swift、Cheese.swift、Olives.swift、Double.swift、Functions.swift に記述してください。main.swift には提供された入出力コードが含まれており、編集できません。

自分で試してみよう

// 提供された入出力コード: そのままにしておく
var input: [String] = []
while let line = readLine() { input.append(line) }
for line in input {
    let names = line.isEmpty ? [] : line.split(separator: ",").map(String.init)
    let p = build(names)
    let tops = p.toppings()
    print("\(p.price()): " + (tops.isEmpty ? "plain" : tops.joined(separator: ", ")))
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン

自分で練習してみよう: Swiftプレイグラウンド