Double Dispatch (Vistor pattern) に関する考察 (2)
おお,どうやら id:xoinu:20070629 では,Visitor と Acceptor の名前を入れ違えていたようだ。どおりで Visitor が visit しないで,Acceptor が visit してるわけだ…(笑)。そもそも double dispatch の説明をするために,より知名度の高い Visitor を覚えておこうと復習のために参考にしたサイトが,間違っていたという恥ずかしい話なのだが。なるほど,分かったよ Visitor。ちゃんと訪れているじゃないか Visitor。orz。お陰で,けっこう萎えたのだが,昨日までに考察したことを思い出して書いてみる。→ 混乱していて申し訳ない。Visitor パターンでは,Acceptor のオブジェクトが Visitor のメソッドに visit するのが正しいらしい。全然納得いかないけど,そういうものらしい。絶対逆にしたほうが分かりやすいって。
(閑話休題・笑)
昨日の例では,動物は「俺食べる=餌食べられる」ということを理解していた。では新しく「俺餌投げる」という機能を追加したくなったらどうしたら良いだろうか。動物の例を使うと,Animal に Throw(ThrownObject *pT) メソッドを追加して,pT->ThrownBy(pA) 新しい Acceptor である ThrownObject を追加すれば良さそうに思えてくる。つまり「俺投げる=物投げられる」ということを教えてあげるわけね。
実際,この拡張は上手くいくと場合もある思う。ただここで,コードの再利用について考えてみる。もう無理が生じてるけど,動物のアナロジーで考えてみよう。
新世界の創造主になったつもりで考えよう。「俺は現実世界にも,Second Life にも幻滅した。新しく作る世界では,猫の好物はバナナで,蛇に石を投げさせてやりたい!!」と考えたとき,コードのどの部分をいじれば良いだろうか。食べられて実際何が起こるかは,バナナが知っていたことを思いだすと,バナナと石のコードを書き換えれば良いことが分かる。すると,どうやら再利用できるものと,そうでないモジュールの境界は下のようになりそうだ。
おお,なんかいい感じで分離できている。これはこれで分かり易いけれど,実はまだ不十分な点がある。それは,動物の行動と,餌や投げられるもの(Acceptor)が対で相互に依存しているところだ。
ある日,仮想世界を気に入ったとある友人が「動物の写真集を作りたい」と話を持ちかけてきた。さあどうする。彼は彼の世界で動物たちを「印刷」したいんだけど,別に餌を食べたり,物を投げたりしてくれる必要はない。彼からすると Eat とか Throw といった関数は,無駄な機能ってことになる。むしろその2つは要らないから PrintedBy(Visitor)メソッドと,PrintAnimal クラス(Acceptor)が欲しいというかも知れない。このデザインでは,動物たちのデータ(model?)と,機能(controller?)が分離できていない。
昨日,double dispatch および Visitor は平たくいうと「2種類の抽象的な型しか分かっていない object があったときに,実行時の実際の型を判別して,その組み合わせに応じて何かをする」ものだと書いた。結局その仕組みを提供しているだと割り切ることにする。
せっかく作りかけた動物たちだけど「食べる」や「投げる」といった機能は,コードの再利用を考えると具体的すぎると思う。それを分離したのが以下のコードである。一応,むかしのバナナの実装も比較のために残しておいた。Eat に相当する関数は Dispatch という味気ないものにした。EatenBy に相当する関数も Dispatch。Food は Dispatcher。Feed は今や Dispatcher 次第でなんでも出来るので,DoSomething とした。Dispatch が2回呼ばれてDouble Dispatch。どうかね?
#include <iostream> #include <stdexcept> #include <memory> class Cat; class Dog; class Monkey; class Snake; //***************************************************************************** // Acceptor //***************************************************************************** class Dispatcher { public: virtual ~Dispatcher(){} virtual void Dispatch(Cat *pC) = 0; virtual void Dispatch(Dog *pD) = 0; virtual void Dispatch(Monkey *pM) = 0; virtual void Dispatch(Snake *pS) = 0; }; //***************************************************************************** // Visitor //***************************************************************************** class Animal { public: virtual ~Animal(){} virtual const char * Name() = 0; virtual void Dispatch(Dispatcher *pF) = 0; }; //***************************************************************************** // This function does not know what animal pA is nor what food pF is. //***************************************************************************** void DoSomething(Animal *pA, Dispatcher *pF) { try { pA->Dispatch(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 Dispatch(Dispatcher *pF) { pF->Dispatch(this); } virtual const char * Name() { return "cat"; } void IgnoreMaster() { /*...*/ } }; class Dog : public Animal { public: virtual void Dispatch(Dispatcher *pF) { pF->Dispatch(this); } virtual const char * Name() { return "dog"; } void FollowMaster() { /*...*/ } }; class Monkey : public Animal { public: virtual void Dispatch(Dispatcher *pF) { pF->Dispatch(this); } virtual const char * Name() { return "monkey"; } void Walk() { /*...*/ } }; class Snake : public Animal { public: virtual void Dispatch(Dispatcher *pF) { pF->Dispatch(this); } virtual const char * Name() { return "snake"; } void Twist() { /*...*/ } }; //***************************************************************************** // Concrete Acceptor //***************************************************************************** class Banana : public Dispatcher { public: virtual void Dispatch(Cat *pC) { std::cerr << "Mew.. not good.?n"; pC->IgnoreMaster(); } virtual void Dispatch(Dog *pD) { std::cerr << "Bow.. so-so.?n"; pD->FollowMaster(); } virtual void Dispatch(Monkey *pM) { std::cerr << "Kee.. very good.?n"; pM->Walk(); } virtual void Dispatch(Snake *pS) { throw std::runtime_error("puke!"); pS->Twist(); } }; //***************************************************************************** class Nattou { public: Nattou(){} void EatenBy(Animal *pA); protected: virtual void EatenByCat(Cat *pC) { throw std::runtime_error("puke!"); pC->IgnoreMaster(); } virtual void EatenByDog(Dog *pD) { throw std::runtime_error("puke!"); pD->FollowMaster(); } virtual void EatenByMon(Monkey *pM) { std::cerr << "Kee.. very good.?n"; pM->Walk(); } virtual void EatenBySnk(Snake *pS) { throw std::runtime_error("puke!"); pS->Twist(); } }; 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); } }; try { NattouDispatcher dispatcher(this); pA->Dispatch(&dispatcher); } catch (const std::runtime_error &ex) { std::cerr << "?nA " << pA->Name() << " seems not to like the food!?n" << "It results in ... " << ex.what() << '?n'; } } //***************************************************************************** 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<Dispatcher> pF(new Banana); DoSomething(pA1.get(), pF.get()); DoSomething(pA2.get(), pF.get()); DoSomething(pA3.get(), pF.get()); DoSomething(pA4.get(), pF.get()); Nattou zNattou; zNattou.EatenBy(pA1.get()); zNattou.EatenBy(pA2.get()); zNattou.EatenBy(pA3.get()); zNattou.EatenBy(pA4.get()); return 0; }
これで,バナナでも石でも何でも投げ込んで Double Dispatch。関数の名前を変えただけじゃないかといわれれば,それまでだ。関数のプロトタイプが限定されるだけ不便になったといわれれば,その通りだ。const も考慮するとさらに汚くなる。Eat とか Throw が動物に必要な物であるというのが幻想であることが分かったということに過ぎないが,これで,分離したことにしよう。
納豆とバナナの Dispatcher の使い方も微妙に違う。バナナは Dispatcher を継承して,ただ Visitor がやってくるのを待っているだけであるが,納豆は自分自身で Dispatch の方法を知っている,さらに継承を使わずに,ローカル関数内で Dispatcher 使っている。ローカルクラスが無条件で friend になるのは gcc の拡張かな?バナナ式のクラス図(もどき)は以下のようになる。
バナナを拡張していこうと思ったときに,Dispatcher として拡張することになる。それに対して納豆式のクラス図はこんな感じ
微妙な差かもしれないが,納豆のインターフェースのほうが Double Dispatch の仕組みに依存していないので,将来,納豆を継承してひきわり納豆を作るときとかに,余計なことを考えさせずに済むだろう(くどい?)。納豆式のデザインにしておけば,さらに
class Animal : public AnimalModel , public AnimalDispachable { ... };
などとして,特定のユーザには Dispatch 機能を隠蔽してAnimalModelのインターフェースしか提供しない…なんてこともできるかな。
それはさておき,納豆を眺めていると,Double Dispatch や Visitor が何をしているのか,分かった気になる。次回はそれについて思うところを少し書いてみる。