Menu
Coddy logo textTech

デリゲーションパターン

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?. を使用するため、誰も待機していない場合でもオブジェクトは動作します。オブジェクト自身が 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] = []
challenge icon

チャレンジ

簡単

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: " "))
quiz icon腕試し

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

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

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