iPhoneアプリ作成までの道のり9

9. プロトコルデリゲートパターン

プロトコルデリゲートパターンはCocoaUIにおいて最も基本的なイベント伝達のパターンなので、既に御馴染みと言っても良いだろう。例えばUIApplicationDelegateがそうだし、UIWebDelegateがそうである。重要なのは、このプロトコルデリゲートパターンを、自分で作るクラス(UIに限らず)にも同じように適用できるということだ。そのためには以下のシンプルな手順を踏めば良い。

適用の機会は、あるクラスが、自分の中で起きたイベントを誰かに通知する必要がある場合である。そして、通知した先の動作結果によっては、自身の処理内容を変える必要がある場合にも使える。

・どのような通知を行うのかをプロトコルとして定義する
 (ここでいうプロトコルは概念的なものでなく、実際のObjective-Cの@protocolである)

・イベント通知元のクラスにプロトコルインスタンスを持たせて、そのインスタンスをプロパティ等で外からセットできるようにしておく

・イベント通知元のクラスは、必要なタイミングで、プロトコルに定義されたメッセージを送信する

・イベント通知先のクラスにプロトコルの実装を行う

・イベント通知先のクラス自身か、あるいはその上位のクラスで、イベント通知元オブジェクトにイベント通知先オブジェクトをセットする


これでプロトコルデリゲートパターンの完成だ。この設計のメリットは、デリゲートの切り替えができることにある。即ち、イベント通知元のオブジェクトが色々なところで使い回せる可能性を高めるのだ。

例えば、通信ゲームの戦闘システムを考えてみよう。
ある戦闘システムを別のゲームでも使い回せるようにしたいとする。ダメージ判定などの処理はサーバー側で行うこととする。この場合、クライアント側のアプリは、通信システムとレンダリングシステムを別々に設計して疎結合にするのが望ましい。以下にコードのスケッチを示す。

// 戦闘システムのプロトコルを定義する
@protocol MyBattleSystemDelegate<NSObject>
-(void)hitYourAttack:(int)damage as:(int)action;
-(void)hitEnemyAttack:(int)damage as:(int)action;
-(void)endBattle:(BOOL)win;
@end

// 戦闘システムのインターフェイス
@interface MyBattleSystem : NSObject
{
    id<MyBattleSystemDelegate> _delegate;
}

@property(assign, nonatomic) id<MyBattleSystemDelegate> delegate;

-(void)move:(CGPoint)point;
-(void)attack:(int)action;

@end

// ゲームマネージメントクラスの定義
// このクラスをデリゲートの実装とする
@interface MyGame : NSObject<MyBattleSystemDelegate>
{
    MyBattleSystem* battleSystem;
}
-(void)startBattle;

@end

// ゲームマネージメントクラスの実装
@implementation MyGame

// 戦闘開始
-(void)startBattle {
    battleSystem = [[MyBattleSystem alloc] init];
    battleSystem.delegate = self;
}

// ユーザーが攻撃したことをバトルシステムに通知する
-(void)didUserAttack:(int)action {
    [battleSystem attack:action];
}

// ユーザーが動いたことをバトルシステムに通知する
-(void)didUserMove:(CGPoint)point {
    [battleSystem move:point];
}

// プロトコルの実装
-(void)hitYourAttack:(int)damage as:(int)action {
    // ユーザーデータを更新
    // レンダリングシステムにダメージ表示を指示
}

-(void)hitEnemyAttack:(int)damage as:(int)action {
    // ユーザーデータを更新
    // 死亡判定
    // レンダリングシステムに被弾モーションの表示を指示
    // レンダリングシステムにダメージ表示を指示
}

-(void)endBattle:(BOOL)win {
    if (win) {
        // 勝利
    }
    else {
        // 敗北
    }
    
    [battleSystem release];
    battleSystem = nil;
}

@end

MyBattleSystemクラスは、MyBattleSystemDelegateプロトコルにのみ依存しているのであって、MyGameクラスに全く依存していないことが重要である。