ホーム < ゲームつくろー! < デザインパターン習得編
Double Dispatch
〜抽象オブジェクトがお互いを知れる!〜
例えば3種類のキャラクタ(A,B,C)がいたとします。AはBに合うとBを食べようとし、BはAから逃げようとします。BはCに合うと笑い、CはBに合うと怒ります。CはAに合うと踊り、AはCに拍手を送ります。また同じ種族に合うと挨拶をするとします。いわゆるライフゲームですね。この3種類のキャラクタを世界に撒いてしばらく見ていると、どこかでお互いに出会う機会がやってきます。出会った時に各キャラクタは「Actionメソッド」でそれぞれの振る舞いをするとします。
Aのアクションは「挨拶をする、食べようとする、踊る」です。これら具体的な行動をActionメソッド内で呼び出したいわけです。しかし、世界に撒かれているキャラクタは多分「Characterクラス」という抽象クラスでしか認識できません。AはぼんやりとしたCharacterクラスから具体的に相手を識別して具体的なアクションメソッドを呼び出す必要があるわけです。
Double Dispatch(ダブルディスパッチ:2重発送)はまさにこういう状況を解決する一つの方法として知られています。
@ 引数で相手を識別する
ダブルディスパッチの一番簡単な例を挙げます。例えば次のメソッドをご覧ください:
void Character_A::Dispatch( Character* other ) {
other->Action( this );
}
Character_AクラスはCharacterクラスから派生されたクラスです。上はCharacter_A::Dispatchメソッドの中身を見ています。引数に抽象的なCharacterオブジェクトが渡されています。Aはその抽象クラスが持つActionメソッドを内部で呼び出して「自分自身を表すthis」を渡しています。なぜこのような事をするのでしょうか?
クラスの中でのthisはもちろん自分自身の型になっています。上の場合はthisはCharacter_A型であるわけです。という事は、other->Action( this )の呼び出しをすると、「引数がAであるActionメソッド」が自動的に呼ばれる事になります。ここが面白いんです。otherの立場に立つと、具体的なCharacter_A型のオブジェクトを引数に持ったActionメソッドが呼ばれているのですから、その中でAに対するアクションを呼び出せばいいんです。例えばCharacter_B::Actionメソッドは次のようになります:
void Character_B::Action( Character_A* other ) {
// Aに合うと逃げる!
EscapeFromA( other );
}
AのDispatchメソッドにBが渡された場合、上のメソッドが呼ばれます。Cが渡された場合はCのAに対するActionメソッドが呼ばれるわけです。「自分のメソッドの中で引数に渡されたオブジェクトに対し自分自身を渡す」。この2重の呼び出しを「ダブルディスパッチ」と言います。
A ダブルディスパッチ
上の原理をうまく使うと、抽象オブジェクトのDispatchメソッドを呼ぶだけで具体的なアクションを起こす事ができるようになります。各クラスの実装は次のようになります:
class Character_A;
class Character_B;
class Character_C;
class Character {
public:
virtual void Dispatch( Character* other ) = 0;
protected:
virtual void Action( Character_A* other ) = 0;
virtual void Action( Character_B* other ) = 0;
virtual void Action( Character_C* other ) = 0;
};
class Character_A : public Character {
public:
virtual void Dispatch( Character* other ) {
other->Action( this );
}
protected:
virtual void Action( Character_A* other ) { Greet( other ); // 挨拶する }
virtual void Action( Character_B* other ) { Eating( other ); // 食べる }
virtual void Action( Character_C* other ) { Clap( other ); // 拍手する }
}
class Character_B : Character {
public:
virtual void Dispatch( Character* other ) {
other->Action( this );
}
protected:
virtual void Action( Character_A* other ) { Dance( other ); // 踊る }
virtual void Action( Character_B* other ) { Greet( other ); // 挨拶する }
virtual void Action( Character_C* other ) { Laugh( other ); // 笑う }
}
class Character_C : Character {
public:
virtual void Dispatch( Character* other ) {
other->Action( this );
}
protected:
virtual void Action( Character_A* other ) { Escape( other ); // 逃げる! }
virtual void Action( Character_B* other ) { Anger( other ); // 怒る }
virtual void Action( Character_C* other ) { Greet( other ); // 挨拶する }
}
注目はCharacterクラスです。親クラスなのですが内部に子クラスを引数に取るActionクラスを純粋仮想メソッドとして定義しています。よって、子クラスはこれらのメソッドをすべて実装する必要があります。今度は子クラスに注目です。例えばCharacter_AクラスのActionメソッドは、引数によって呼び出す具体的なアクションを決めています。ちゃんと相手を見て「挨拶する、食べる、拍手する」となっていますね。他の子クラスも同様に同じActionメソッド内で具体的な行動が呼び出されています。
つまり、ダブルディスパッチを使うと、各クラスで同じメソッドの引数相手に対して具体的な振る舞いを記述できるようになるわけです。これにより、振る舞いの局所化が可能になります。
B ダブルディスパッチの使い方
上のようなダブルディスパッチクラスは例えば次のように使われます:
void WorldManager::update() {
// 世界にいる人たち同士が出会った場合、それぞれの行動をしてもらう
int sz = worldCharacterAry.size();
for ( int i = 0; i < sz; i++ ) {
for ( int j = i + 1; j < sz; j++) {
Character* P = worldCharcterAry[ i ];
Character* Q = worldCharcterAry[ j ];
// 出会ったかな?
if ( P->IsMeet( Q ) ) {
P->Dispatch( Q ); // Pに対する振る舞いを
Q->Dispatch( P ); // Qに対する振る舞いを
}
}
}
}
世界の管理人であるWorldManagerは、世界中のキャラクタをworldCharacterAry配列で一元管理しています。この配列はもちろんCharacter*型です。パフォーマンスの事は目をつむるとして、総当りを行ってお互いに出会っている者を探します。もし出会っていたら双方のDispatchメソッドを呼び出してあげます。これにより、お互いが相手を見てそれぞれの振る舞いを勝手に行ってくれるわけです。
このように、ダブルディスパッチは抽象的なオブジェクト同士を出会わせて具体的な振る舞いをさせる事が可能な設計になってます。
C 便利ですけど注意点も
ダブルディスパッチは便利です。特に衝突判定などで絶大な力を発揮します。しかし注意点もあります。まず、親クラスが自分の派生クラスである子クラスを知っている必要があり、子クラスを引数に持つメソッドを宣言する必要があります。これにより、子クラスが追加されたら、親クラスをはじめ派生した全ての子クラスに新しい子クラスへの対応を書く必要にかられます。例えば先の例で、キャラクターDが追加されたら、Dに対するすべての振る舞いを各クラスに実装する必要が出てくるわけです。よって派生クラスが増える度に、実装しなければならないメソッドの数が2次関数的に増えていきます。
これを少し解消する方法はあります。先の例では親クラスのActionメソッドが純粋仮想メソッドになっていましたが、これを空実装にします。こうすると、相手にしなくても良いキャラクタへの実装を派生クラスで行う必要がなくなります。キャラクタDが皆から無視されてしまう悲しいキャラクタだった場合、A,B,Cクラス内にDに対するActionメソッドを入れる必要が無くなるわけです。これにより、必要な相手に対してだけ具体的な振る舞いを書けば良いだけになります。
それでも、親クラスが全ての派生クラスを知っている必要があるのは大きな制約です。一般に、ダブルディスパッチは拡張性があまりありません。ある程度固定的なキャラクタや物に対してのみ使う事が多いです。衝突判定などではプリミティブの数が固定的である程度限定されているので、ダブルディスパッチを適用できる絶好な例です。欠点を理解して使いこなすと便利なのがダブルディスパッチです。