デリゲーションパターン
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.pngdelegate プロパティはオプションであり、呼び出しには delegate?. を使用するため、誰も待機していない場合でもオブジェクトは動作します。オブジェクト自身が delegate を保持して存続させないように weak として宣言されており、互いを参照するオブジェクト間の保持サイクルを回避できます。
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"]デリゲーションはオブザーバーパターンとは異なります。オブジェクトには、質問し通知する1つのデリゲートがあります。一方、サブジェクトには、通知だけを行う複数のオブザーバーを持つことができます。
// delegation: 1人のパートナー、質問に答えることがある
weak var delegate: TableDelegate?
// observer: 任意の数のリスナー、通知のみ
private var observers: [(Event) -> Void] = []チャレンジ
簡単Checkoutはデリゲートに判断を求めます。プロトコルCheckoutDelegate(クラス専用)には、discount(for total: Int) -> IntとdidComplete(total: Int)があります。Checkoutにはweak var delegateがあり、pay(_ prices: [Int]) -> Intは価格を合計し、デリゲートの割引額(デリゲートがない場合は0)を差し引き、最終合計をデリゲートに通知して返します。合計が100以上の場合に10%割引を提供し、完了したすべての合計をreceiptsに記録するデリゲート、LoyaltyShopを実装してください。
提供されたコードは、カンマ区切りの価格が記された行を読み取ります。最初の行はデリゲートなしで支払い、残りの行は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: " "))
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
自分で練習してみよう: Swiftプレイグラウンド