Menu
CoddyTech

委托模式

本课属于 Coddy Swift 学习旅程的“面向对象编程”单元。第 46 课,共 57 课。

委托让一个对象通过协议,将部分决策交给另一个对象,也就是它的委托对象。这个对象完成自己的工作,并在需要灵活处理的环节询问委托对象。Apple 的框架中随处可见这种模式:

protocol DownloadDelegate: AnyObject {
    func progress(_ percent: Int)
    func finished(_ file: String)
}

final class Downloader {
    weak var delegate: DownloadDelegate?

    func download(_ file: String) {
        for p in [50, 100] { delegate?.progress(p) }
        delegate?.finished(file)
    }
}

final class Screen: DownloadDelegate {
    func progress(_ percent: Int) { print("\(percent)%") }
    func finished(_ file: String) { print("done: \(file)") }
}

let screen = Screen()
let d = Downloader()
d.delegate = screen
d.download("photo.png")

输出:

50%
100%
done: photo.png

delegate 属性是可选的,调用时使用 delegate?.,因此即使没有人在监听,该对象也能正常工作。它被声明为 weak,这样对象本身就不会让其 delegate 保持存活,从而避免相互指向的对象之间形成强引用循环:

weak var delegate: DownloadDelegate?   // 要求协议只能由类遵循:AnyObject
delegate?.finished(file)               // 没有 delegate 时不会执行任何操作

委托也可以回答问题,这样它就能自定义对象的行为,而无需创建子类:

protocol ListDelegate: AnyObject {
    func shouldShow(_ item: String) -> Bool
}

final class ListView {
    weak var delegate: ListDelegate?
    func render(_ items: [String]) -> [String] {
        items.filter { delegate?.shouldShow($0) ?? true }
    }
}

final class HideDrafts: ListDelegate {
    func shouldShow(_ item: String) -> Bool { !item.hasPrefix("draft") }
}

let view = ListView()
let rules = HideDrafts()
print(view.render(["a", "draft b"]))
view.delegate = rules
print(view.render(["a", "draft b"]))

输出:

["a", "draft b"]
["a"]

委托与观察者模式不同:一个对象只有一个委托对象,会向它提问并通知它;而一个主题可以有多个观察者,只会通知它们。

// 委托:一个协作对象,可以回答问题
weak var delegate: TableDelegate?
// 观察者:可以有任意数量的监听者,只接收通知
private var observers: [(Event) -> Void] = []
challenge icon

挑战

简单

Checkout 会向它的委托对象请求决策。协议 CheckoutDelegate(仅限类使用)包含 discount(for total: Int) -> Int 和 didComplete(total: Int)。Checkout 有一个 weak var delegate,而 pay(_ prices: [Int]) -> Int 会对价格求和,减去委托对象给出的折扣(没有委托对象时为 0),告知委托对象最终总额并返回该总额。请实现 LoyaltyShop,让它作为委托对象,为总额达到或超过 100 的订单提供 10% 的折扣,并在 receipts 中记录每笔已完成订单的总额。

提供的代码会读取多行以逗号分隔的价格。第一行不使用委托对象付款,其余行使用 LoyaltyShop 付款,打印每笔订单的总额,最后打印 receipts: 120 90。

请将代码写在 CheckoutDelegate.swift、Checkout.swift 和 LoyaltyShop.swift 中。main.swift 包含提供的输入和输出代码,不能编辑。

亲手试试

// 提供的输入输出代码:保持原样。
var input: [String] = []
while let line = readLine() { input.append(line) }
let checkout = Checkout()
let shop = LoyaltyShop()
for (i, line) in input.enumerated() {
    if i == 1 { checkout.delegate = shop }
    print(checkout.pay(line.split(separator: ",").map { Int($0)! }))
}
print("receipts: " + shop.receipts.map { String($0) }.joined(separator: " "))
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Swift 在线编辑器