Double Dispatch (Vistor pattern) に関する考察 (1)
Double dispatch は,よく Visitor パターンの説明で出てくるが,visitor.visit() でビジターが重要な理由を,いまいち理解していない。今手元に原著がないので分からないが,どうやらWebで参考にしたページが,Visitor と Acceptor を逆にして説明していたのが原因と思われる。以下の文章は私の理解に基づいて変更した。Visitor が Accepter に accept されて visit するのが正しいよね?→ どうやら理解できなかった最初のやつが正しいようだ。Visitor に処理があって,Acceptor は Visitor を仲介するだけの存在らしい。いや,どう考えても Acceptor が Visitor のコードに visit してないかい!?ま,いいか。
まぁ,むしろ大切なのは double dispatch の概念を理解することなので,間違いは放っておく。
Double dispatch というのは,平たくいうと「2種類の抽象的な型しか分かっていない object があったときに,実行時に型を判別して,その組み合わせに応じて何かをする」ための仕組みを,if 文や dynamic_cast などを用いずに,かつ後で保守性の高い形で提供するものだ。以下のコードを見てみよう。自然言語で説明すると情けなくなっちゃうので,細かい説明は割愛するが,恥ずかしげもなく,動物に餌を与えるサンプルコードを書いてみた。
#include <iostream> #include <stdexcept> class Food {}; class Animal { public: virtual ~Animal(){} virtual const char * Name() = 0; virtual void Eat(Food *pF) = 0; }; //***************************************************************************** // This function does not know what animal pA is nor what food pF is. //***************************************************************************** void Feed(Animal *pA, Food *pF) { try { pA->Eat(pF); } catch (const std::runtime_error &ex) { std::cerr << "?nA " << pA->Name() << " seems not to like the food!?n" << "It results in ... " << ex.what() << '?n'; } }
とりあえず,Feed の中で「動物が,餌を,たべる」ということを表現してみた。一応このコードはこれで正しくて g++ -c コマンドでコンパイルできる。次に,どんな動物がいるだろうか。考えてみる。奮発して 4 匹定義してみる。
class Cat : public Animal { public: virtual void Eat(Food *pF) { /* eating... */ } virtual const char * Name() { return "cat"; } void IgnoreMaster() { /*...*/ } }; class Dog : public Animal { public: virtual void Eat(Food *pF) { /* eating... */ } virtual const char * Name() { return "dog"; } void FollowMaster() { /*...*/ } }; class Monkey : public Animal { public: virtual void Eat(Food *pF) { /* eating... */ } virtual const char * Name() { return "monkey"; } void Walk() { /*...*/ } }; class Snake : public Animal { public: virtual void Eat(Food *pF) { /* eating... */ } virtual const char * Name() { return "snake"; } void Twist() { /*...*/ } };
名前があることと,食べることの 2 つが,ここでは全動物に共通するもの。その他に,各動物に固有の属性(例えば,蛇だったら『ねじる』みたいな)も,それぞれ追加した。具体的な動物を定義したので,具体的な餌も定義してみる。
class Banana : public Food {};
バナナだ。
動物と餌が定義できて,ようやく準備完了だ。実際に食べさせるためのコードを実装することを考えてみよう。
単純だけど,保守の難しい設計
猫にバナナを与えたら,どうなるのか,記述してみよう。単純に考えると Cat::Eat(Food *pF) に書けば良さそうである。
void Cat::Eat(Food *pF) { Banana *pBanana = dynamic_cast<Banana*>(pF); if (pBanana) { // if a cat eats banana... } }
こんな感じか。続いて,犬はどうだ猿はどうだと考えると,けっこう面倒くさいことに気付く。各動物がバナナを食べたときの振る舞いを,各動物の Eat に記述しなければならないからだ。通常,各クラスは別々のファイルに定義されているから,バナナを食べさせるために,全部の動物のコードを変更しなければならないのだ。
新しい食べ物を追加するたびに,全ての動物のコードを変更しなければならない。これは単に面倒くさいから嫌だという理由だけでなくて,各動物のコードを完成させるのに,バナナという餌を知らなければならないという重大な問題がある。これがなんで問題かというと,この動物たちの仮想世界に新しい餌を登場させようと思ったとき,全部の動物たちを,修正しなければならないからである。つまり,動物の実装が全ての餌に依存してしまうという問題がある。
シンプルな double dispatch 〜 餌を活用
そこで登場するのが,double dispatch である。ここまで書いて疲れたので,残りの取りあえずコードを引用してから説明することにする。さっきの実装では何もしなかった餌に,少し働いてもらうことにする。キモはこれ。
class Cat; class Dog; class Monkey; class Snake; class Food { public: virtual ~Food(){} virtual void EatenBy(Cat *pC) = 0; virtual void EatenBy(Dog *pD) = 0; virtual void EatenBy(Monkey *pM) = 0; virtual void EatenBy(Snake *pS) = 0; };
先ほどの設計では,動物に餌のことを,全てを教えなければならなかったために,保守が面倒なことになった。double dispatch を使う実装では,先ほど何もしなかった餌クラスに,世の中にどんな動物がいるのか,その名前だけを教えてやる。正確にいうと,餌はそれが動物かどうかも知らない。この世の中には猫,犬,猿,蛇なるものがいて,それに食べられることだけを知っているだけである。
これにともない,食べる側のコードをも以下のように変更する。
class Cat : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "cat"; } void IgnoreMaster() { /*...*/ } }; class Dog : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "dog"; } void FollowMaster() { /*...*/ } }; class Monkey : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "monkey"; } void Walk() { /*...*/ } }; class Snake : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "snake"; } void Twist() { /*...*/ } };
以前の実装では,各動物たちは,食べ物にはどんな種類があるのか,食べるとどうなるのかを自分自身が知っていたけれど,double dispatch を使うときは,「俺が餌を食べるってことは,餌が俺に食べられるなんだな」ということしか知らない。動物たちは,その餌を食べると自分がどうなるのか分からないのはちょっと間抜けな話だが,それはこの例え話が悪いせいである。
そのかわり,バナナが全てを知っている。
class Banana : public Food { public: virtual void EatenBy(Cat *pC) { std::cerr << "Mew.. not good.?n"; pC->IgnoreMaster(); } virtual void EatenBy(Dog *pD) { std::cerr << "Bow.. so-so.?n"; pD->FollowMaster(); } virtual void EatenBy(Monkey *pM) { std::cerr << "Kee.. very good.?n"; pM->Walk(); } virtual void EatenBy(Snake *pS) { throw std::runtime_error("puke!"); pS->Twist(); } };
バナナのコードは抽象的な餌とは違って,それぞれの動物がどんな固有の性質(IgnoreMaster,Twist など)を持っているかを知っている。そして食べられると具体的にどうなるのかを,ここ記述する。
全部をつなげた完全なコードは以下の通り。
#include <iostream> #include <stdexcept> #include <memory> class Cat; class Dog; class Monkey; class Snake; //***************************************************************************** // Acceptor //***************************************************************************** class Food { public: virtual ~Food(){} virtual void EatenBy(Cat *pC) = 0; virtual void EatenBy(Dog *pD) = 0; virtual void EatenBy(Monkey *pM) = 0; virtual void EatenBy(Snake *pS) = 0; }; //***************************************************************************** // Visitor //***************************************************************************** class Animal { public: virtual ~Animal(){} virtual const char * Name() = 0; virtual void Eat(Food *pF) = 0; }; //***************************************************************************** // This function does not know what animal pA is nor what food pF is. //***************************************************************************** void Feed(Animal *pA, Food *pF) { try { pA->Eat(pF); } catch (const std::runtime_error &ex) { std::cerr << "?nA " << pA->Name() << " seems not to like the food!?n" << "It results in ... " << ex.what() << '?n'; } } //***************************************************************************** // Concrete Visitor //***************************************************************************** class Cat : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "cat"; } void IgnoreMaster() { /*...*/ } }; class Dog : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "dog"; } void FollowMaster() { /*...*/ } }; class Monkey : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "monkey"; } void Walk() { /*...*/ } }; class Snake : public Animal { public: virtual void Eat(Food *pF) { pF->EatenBy(this); } virtual const char * Name() { return "snake"; } void Twist() { /*...*/ } }; //***************************************************************************** // Concrete Acceptor //***************************************************************************** class Banana : public Food { public: virtual void EatenBy(Cat *pC) { std::cerr << "Mew.. not good.?n"; pC->IgnoreMaster(); } virtual void EatenBy(Dog *pD) { std::cerr << "Bow.. so-so.?n"; pD->FollowMaster(); } virtual void EatenBy(Monkey *pM) { std::cerr << "Kee.. very good.?n"; pM->Walk(); } virtual void EatenBy(Snake *pS) { throw std::runtime_error("puke!"); pS->Twist(); } }; //***************************************************************************** int main(int argc, char **argv) { std::auto_ptr<Animal> pA1(new Cat); std::auto_ptr<Animal> pA2(new Dog); std::auto_ptr<Animal> pA3(new Monkey); std::auto_ptr<Animal> pA4(new Snake); std::auto_ptr<Food> pF(new Banana); Feed(pA1.get(), pF.get()); Feed(pA2.get(), pF.get()); Feed(pA3.get(), pF.get()); Feed(pA4.get(), pF.get()); return 0; }
たとえ話はそろそろやめにしよう。このコード,特に Feed が呼び出されるところのポイントは
- Feed が呼び出されたときには,まだ動物と,餌であることしか知らない(知らなくて良い)
- Feed 内の pA->Eat(pF) の仮想関数呼び出し(仮想関数テーブルの値)で,pA の型が判明(最初の dispatch)
- Eat 内の pF->EatenBy(*this) の仮想関数呼び出し(仮想関数テーブルの値)で,pF の型が判明。既に型が判明している *this を引数に渡して,めでたく,動物と餌の両方が同定される(二度目の dispatch)
- 実際の処理は二度目の dispatch 先の EatenBy に記述されている。
ということである。また,これによって何が改善されたかというと
- 新しい食べ物を追加するときは,Food クラスの導出クラスを実装するだけで良くなった。
- 動物クラスの実装が,個々の食べ物に依存しなくなった。
点が上げられる。一般にこのパターンだと,動物を増やすのが大変だとか思われがちだけど,動物を増やすときは,状況によって以下の2通りのシナリオを選択できる。
- Food クラスに純粋仮想関数を追加してコンパイルエラーを生じさせる。
- Food クラスを継承して新たな抽象 Food クラスを作る。変更が必要な動物だけ変更。今までのコード,バイナリは全て動く。
どうなんだろ。今のところ2のケースで運用した経験はない。まあ基本的に Visitor を追加していくような場合に適用すべきパターンであることは確かだ。
また,double dispatch の説明でよく混同しがちなことだが,double dispatch を構成する要素は3つある。
- 型が分からないけど,何かする役(Visitor パターンのクライアント)
- 最初の dispatch 先の仮想関数 = double dispatch 先へ誘導する役(Visitor パターンの Visitor)
- 実際の処理が書かれた double dispatch 先の役(Visitor パターンの Acceptor。Visitor が visit する先)
Visitor パターンは1〜3が全て分離されているが, 1 と 3 を同じクラスにしたり,全部同じにした実装もできる。こういうことを考えると,Visitor パターンは,double dispatch の特殊なパターンであると言える。どう考えても,キモは visit することじゃなくて double dispatch だと思うんだよね。
モジュールの依存関係を考えてみると,どうやら double dispatch 先の処理が書かれた ConcreteAcceptor は Visitor および ConcreteVisitor に依存することになりそうである。これは本当だろうか。そうだとしたらどのくらい依存するのだろうか。
次回は各モジュールの依存関係について,少し考察してみようと思う。