Menu
CoddyTech

装饰器模式

本课属于 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(将内部所有内容的价格加倍,并将配料重复两次)。完成 build(_:),它从 Base 开始,并按名称顺序逐个套上装饰器。

提供的代码会读取多行内容,每行包含以逗号分隔的配料名称(也可以是空行),然后构建每个披萨并输出 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 在线编辑器