Double Dispatch (Visitor pattern) に関する考察 (3)
id:xoinu:20070629,id:xoinu:20070630:1183260530 の続き。で,ようやく私なりに考えて,double dispatch(Visitor パターン)について思うことを2つ書いてみたい。
Visitor パターンの幻想?
Visitor パターンの本質は double dispatch にある。で,double dispatch の本質は多態を使って条件文(if,switch)を無くすことだ。Web を見ていたら,Visitor パターンの役割はデータ構造と処理の分離であるとあったけど,それってデータのオブジェクトをポインタで渡して,渡した先で何か処理すしてるだけぢゃ…と言いたくなる。それは,たまたまそうなっているだけだと思う。
double dispatch のメリット
私がここまで興味を持って考えたのは,保守性の高いライブラリを設計するにあたって,double dispatch というのは役に立つんじゃないかと考えているからだ。けっきょく,double dispatch によって何が変わったのか?それは Nattou::EatenBy に集約されている。
void Nattou::EatenBy(Animal *pA) { class NattouDispatcher : public Dispatcher { Nattou *_this_; public: explicit NattouDispatcher(Nattou *p) : _this_(p){} virtual void Dispatch(Cat *pC) { _this_->EatenByCat(pC); } virtual void Dispatch(Dog *pD) { _this_->EatenByDog(pD); } virtual void Dispatch(Monkey *pM) { _this_->EatenByMon(pM); } virtual void Dispatch(Snake *pS) { _this_->EatenBySnk(pS); } }; NattouDispatcher dispatcher(this); pA->Dispatch(&dispatcher); }
結局これは,やってることは
void Nattou::EatenBy(Animal *pA) { do { { Cat *pC = dynamic_cast<Cat*>(pA); if (pC) { EatenByCat(pC); break; } } { Dog *pD = dynamic_cast<Dog*>(pA); if (pD) { EatenByDog(pD); break; } } : : } while (0); }
と,全く一緒。このやり方では,dynamic_cast が遅いというなら,Animal に enum 型のタイプを返すメソッドを用意しておいて
void Nattou::EatenBy(Animal *pA) { switch (pA->Type()) { case CAT: EatenByCat(static_cast<Cat*>(pA)); break; case DOG: EatenByDog(static_cast<Dog*>(pA)); break; case MONKEY: EatenByMonkey(static_cast<Monkey*>(pA)); break; case SNAKE: EatenBySnake(static_cast<Snake*>(pA)); break; default: assert(!"Never reach here."); break; } }
とすれば,恐らく double dispatch(仮想関数テーブル)よりも高速な実装になる。C++で douoble dispatch をやる場合には,一旦別のクラスのコードに処理が移るぶんだけ,複雑になっている。多くの場合は Type() と switch で十分だろう。
しかし,switch のやり方だと,基底クラスに人為的な enum と,タイプメンバを持たせるという無駄が生じてしまうのと,C++ の抽象クラスの多態性を活用せずに実行時のC++の型情報を無視して static_cast してしまっている。さらに,条件文を書き下す方式だと,どうしても人為的なミス,修正し忘れ,モレが生じてしまうことを免れない。
プロジェクトを引き継ぐ際に後任者に「Animal の種類を追加するときは,Type() で分岐している switch 文と if 分の箇所を,全部修正してね」なんて教えるなんて事態(よくある)は,プログラマとしては敗北感を感じずにはいられない。
その点,double dispatch の場合は,おおざっぱな話,新しい動物を追加する際には Dispatcher に純粋仮想関数を追加してビルドしてエラーが全部無くなるように実装するだけで,動物が追加できる。つまり条件文のシンタックスを関数という形に変換することで,コンパイル時に switch 文の実装漏れを検出できるようになる。