エムティーアイ エンジニアブログ

株式会社エムティーアイのエンジニア達による技術ブログ

Delegateについてのまとめ

こんにちは。FinTechサービス部エンジニアの松原と申します。
部署に配属されてから、初めてiOS開発に触れて苦戦しております。

さてiOSアプリのフレームワークであるCocoa Touchの中では、頻繁にdelegateが出てきます。 最初はなんとなく雰囲気で使っていましたが、しっかり理解しようと思い記事にまとめてみました。 よろしければご覧いただけますと幸いです。

主な参考資料

The Swift Programming language (swift4)のDelegation

Cocoa Core CompetenciesのDelegation

概要

自分の処理の一部分を、他人に任せてやってもらうこと
もう少しプログラムチックに言うと、自オブジェクトの処理の一部を別オブジェクトに任せること
つまり振る舞いの一部を外部依存にして、依存先に応じて、複数の振る舞いを柔軟に持たせることが可能。
連携は(Swiftでは)プロトコルを利用して疎結合になっているので、拡張性が高く、変更に強い。そんなデザインパターン。

ロール

3役ある。

  1. 他人に依存する部分があるAさん(処理を任せるオブジェクト。デリゲート元)
  2. Aさんに依存されるBさん(処理を任されるオブジェクト。デリゲート先)
  3. Aさんが他人に委ねたいリスト(任せたい処理を抜き出したプロトコル。デリゲートメソッド)

AさんはBさんのことを考えておらず、ただ任せたい処理だけを決めておけばいい。 つまり助ける人はBさんでもCさんでも可能。
Aさんが他人に委ねたいリストをこなせる人(このプロトコルに準拠するオブジェクト)なら誰でもいい

例えばAさんがお昼ご飯を他人に決めてもらって、調理してもらっている(委譲している)とする。 コードで書くと以下の感じ。

class ASan {
    weak var delegate: ASanDelegate?
    func willStartLunchBreakTime() {
        delegate?.cookingLunch()
    }
}

//Aさんが誰かに任せたいこと(今回はお昼ご飯の決定・調理)
protocol ASanDelegate : class {
    func cookingLunch()
}

そして別の人例えばBさんは、Aさんのお昼ご飯を決めて代わりに調理することができる 今回は唐揚げ弁当をフライパンで調理するという振る舞いにする

class BSan: ASanDelegate {
    func cookingLunch(){
        print("唐揚げ弁当をフライパンでサクッと作る")
    }
}

let aSan = ASan()
let bSan = BSan()
aSan.delegate = bSan
aSan.willStartLunchBreakTime()

ここでAさんはBさん以外の誰からでも柔軟にお昼ご飯を作ってもらえるというのがポイント。 Bさん(あるいはCさん、Dさん、、、、)はメソッドを作成するだけで、 誰もがAさんのお昼ご飯を決定し、調理することができる。 つまり、デリゲート元は実装を変えずに、デリゲート先の数だけ、様々な振る舞いを追加拡張していける。

このように一部の処理を外部に委譲し、セットするデリゲートオブジェクトを変えることで、様々なオブジェクトの振る舞いを、delegate?.fucntionName()一つで呼び出せる。

デリゲートはCocoa、Cocoa Touchにおいて、非常に頻繁に利用されている。一般的にフレームワークオブジェクトがデリゲート元(UITableViewとか)。カスタムコントローラオブジェクトがデリゲート先(自分で作ったViewControllerとか)。

デリゲートによる通知

CocoaTouchにおいてデリゲートはイベント通知の手段としても利用される。 デリゲート元が適切なタイミングでデリゲート先に通知を送り、デリゲート先は、自身や他オブジェクトの状態や見た目を変更すること、あるいは何かしらの値をリターンしてこれに応答する。
例えば上の例では、Aさんからお昼休憩が始まるタイミングで、料理作ってというイベント通知を、他のオブジェクト(Bさん)に対して送っている。イベント通知を受け取ったBさんが、料理をして(printして)それに答えている。このようにデリゲート元が適切なタイミングでデリゲート先のメソッドを呼び出せば、簡単にオブジェクト間で連携できる。

まとめ

Delegateを利用することで、オブジェクト間での連携や通知を容易に実装することができる。
拡張性や変更に強いコードを書ける。

ここから下は参考資料の雑な和訳です。

参考資料1和訳

Delegation
Delegationはデザインパターンである。 クラスと構造体が、それとは異なる型のインスタンスに対して、責任(処理と置き換えて良さそう)のいくつかを渡すこと(あるいは委譲という)を可能にする。 このデザインパターンは、委譲された処理を包むプロトコルを定義することによって実装されている。プロトコルは、委譲された関数を提供することで型が一致する(デリゲートとして知られる。)ことを保証している。 delegationは特定のアクションに応答することができる、また外部ソース(おそらくデリゲート先と思われる)の本来の型を知らなくても外部ソースからデータを取り返すことができる。

以下の例では、サイコロボードゲームに二つのprotocolを定義している。

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGameProtocolは、あらゆるダイスを利用するゲームに適用することができるプロトコルである。
DiceGameDelegateProtocolは、DiceGameの進捗追跡可能なあらゆる型に適用することができるプロトコルである。

制御フローのところで紹介されていたSnakes and Laddersのバージョンに適用すると以下の感じである。 このバージョンだとダイスの目を得るためのDiceインスタンスを使うためにDiceGameProtocolを適用し、 進捗に関して通知するDiceGameDelegateを適用している。

このゲームの詳細は制御フローBreakを参照。

今回のゲームバージョンはSnakesAndLaddersクラスとしてまとめられている。 このクラスはDiceGameProtocolを適用している。取得可能なdiceプロパティと、playメソッドはこのプロトコルに一致させる目的で提供されている。(diceは初期化後に変更される必要がなく参照だけできればいいので、定数プロパティである。)

init()で蛇とはじごゲームのボードセットアップが行われる。全てのゲームロジックはplayメソッドに組み込まれている。 playメソッドは、サイコロが転がった値を提供するためにdiceプロパティを必須でもつプロトコルを利用する。(つまりDiceGame?かな)

delegateプロパティはoptionalのDiceGameDelegateで定義されている。なぜならデリゲートはゲームプレイに必須ではないからだ。したがってoptionalであり、nilで初期化されている。 その後、ゲームインスタンターは任意で、適当なデリゲートを値にセットする

DiceGameDelegateはゲーム進捗を追跡するために3つのメソッドを提供する。これら3つのメソッドは、 ゲームロジックの中に(playメソッドを伴い)組み込まれている。そしてゲーム開始、ターン交代、ゲーム終了のタイミングで呼ばれる。

delegateプロパティがDiceGameDelegate?であるために、このプロパティが使われる場面では、オプショナルチェインで呼び出しを行なっている。もしdelegateプロパティがnilでもerrorにならないで、呼び出しが失敗する。nilじゃなければ、メソッドが呼ばれて、パラメータとして、SnakesAndLaddersインスタンスが渡される

次の例では、DiceGameDelegateProtocolに準拠したDiceGameTrackerクラスを示す。 DiceGameTrackerクラスは、ゲームのターン数を追って行くためにメソッドを利用する。 ゲーム開始時にnumberOfTurnsPropertyはゼロでリセットされ、新しいターンが始まるごとに増加して行く。 そして総ターン数をゲーム終了時に一回だけ出力する。

上記のようにgameDidStarat(_:)の実装は、プレイされるゲームに関する初回情報を表示するためにgameパラメータを利用している。gameパラメータはDiceGame型であり。SnakesAndLadders型ではない。 だからgameDidStart(_:)DiceGameProtocolに準拠したメソッドとプロパティだけにアクセス・利用できる。 しかしながら、そのメソッドはインスタンスの型を怪しむために(その型が何型であるか検証するために)型キャストを依然として利用できる。

例では、gameが本当にSnakesAndLadders型のインスタンスであるか水面下で検証して、もしそうであるなら適切なメッセージをプリントしている。

gameDidStart(_:)メソッドは、gameパラメータを介して、Dicaプロパティにも同様にアクセスできる。 なぜならgameDiceGameProtocolに準拠しているからである。つまりDiceプロパティを持つことが保証されていて、gameDidStart(_:)メソッドは、ダイスの目プロパティにアクセス・プリントできる。 しかもどんなゲームの種類がプレイされているかにかかわらず。

以下にDiceGameTrackerがどのように動いているかを示す。

参考資料2和訳

デリゲートパターンは、あるプログラム中のオブジェクトが、他のオブジェクトと連携したり、代わりに何かを行う、シンプルでパワフルなパターンである。デリゲートするオブジェクト(以下デリゲート元という)は、他のオブジェクト(デリゲートされるオブジェクト、以下デリゲート先という)への参照をもつ。そして適切なタイミングでメッセージ送信、すなわちメソッドの呼び出しを行う。 このメッセージはデリゲート先にイベントを通知する。このイベントはデリゲート元オブジェクトは今まさにハンドルするところである、あるいはちょうどハンドリングが終わったというイベントである。そしてデリゲート先はメッセージに応答する。アプリケーションの中で自分(デリゲート先オブジェクト)あるいは他のオブジェクトの外観や状態を更新することで。 そしていくつかのケースでは、デリゲート先のレスポンスは値を返す。その値は、今まさに起ころうとしているハンドリングされたイベントに影響を及ぼす。デリゲートがもたらす主な価値は、一つの中央オブジェクト内でいくつかのオブジェクトの振る舞いを簡単にカスタマイズできることだ

DelegationとCocoaフレームワーク

デリゲート元は一般的にフレームワークオブジェクトであり、デリゲート先は一般的にカスタムコントローラオブジェクトである。 メモリ環境の管理では、デリゲート先オブジェクトは弱参照で保持される。ガベージコレクション環境では、受信側は、デリゲートを強参照でもつ。デリゲーションはFoundation,UIKit,AppKitなどをはじめとする多くのCocoa、CocoaTouchのフレームワークでめっちゃ使われている。


デリゲート元:AppKitフレームワークのNSWindowインスタンス
NSWindowはprotocolを宣言していて、windowShouldCloseメソッドを定義している。ユーザーがウィンドウ内の閉じるボタンをクリックした時に,windowオブジェクトはwindowShouldCloseをデリゲート先に送る。windowを閉じていいのかを確認するために。デリゲート先はbooleanの値を返す、ウィンドウオブジェクトの振る舞いをコントロールすることによって。

DelegationとNotification

大体のCocoaのフレームワークのデリゲート先は、デリゲート元がNotificationにポストすることでobserverに自動登録される delegate元は唯一、あるメソッドを実装する。この通知メソッドはフレームワーククラスに宣言され、特定の通知メッセージを受け取る。上記のwindowオブジェクトの例では、windowオブジェクトはNSWindowWillCloseNotificatinをObserverに投稿する。しかし、windowShouldCloseメッセージはデリゲート先に送信する。

datasource

datasourceはデリゲートとほぼ一致する。違いはデリゲート元オブジェクトとの関係である。 デリゲートでは、UI制御が委譲される代わりに、datasourceはデータの制御を委譲される。 デリゲート元オブジェクト(一般的にはtableviewのようなViewオブジェクト)はdatasourceの参照を保持する。そして時々尋ねる。データが表示されるべきかどうかを。データソースはデリゲートに似てる。このデータソースはプロトコルと実装を採用する。必要最低限の必須メソッドをプロトコルの中で。 datasourceは応答する。datasourceは、デリゲート元ビューを与えるモデルオブジェクトのメモリ管理に関して責任がある