ホーム < ゲームつくろー! < デザインパターン習得編

Visitor
  〜あなたの内部調べさせてもらいますよパターン


@ 理解超困難パターンは前準備がとっても大事

 Visitorパターン・・・はっきり言って一読では理解に苦しむ難しいパターンです。少しずつ噛み砕いて行きたいと思います。
 まず、Vistorというのは「訪問者」のことです。Vistorパターンは「訪問する」というのがキーワードになります。
 このパターンを使って何ができるのか?Vistorパターンを使うと、ツリー構造(例:ディレクトリ構造、2分木など)のようにつらなっている情報群から、必要な情報だけを抜き出して、それを元に何らかの結果を出すことができます。結果を出すことがこのパターンの目的です。

 イメージしやすいように、RPGのアイテムボックスを例に出します。アイテムボックスの中は種類ごとにきちんと整理されていて、武器袋、防具袋、道具袋に分かれているとします。当然、各袋には該当する種類のアイテムが沢山入っています。


↑絵心は無いんです。勘弁してください(T_T)


これをクラス図で表すと次のようになります。


これはCompositeパターンです。ToolはAddToolというオブジェクトの追加関数を持っています。派生クラスであるBoxクラスは、AddTool関数を実装していて、toolクラスのオブジェクトをどんどん追加できます。これは、上の図で言えば「ToolBox」とか「袋」に相当するわけですね。また、GetChild関数で登録してあるToolオブジェクトにアクセスできます。一方ItemクラスはGetName関数によって名前を取得できますが、AddTool関数やGetChild関数は空実装にしてしまいます。こうすることで、「入れ物」と「アイテム」を区別せずに入れ子が再現できるわけです。このような入れ子状に連なる構造をデータ構造と呼びます。

 さて、今、手持ちのこれらアイテムを全部売りに出すとしましょう。当然合計の売値を計算する必要がありますね。一番手っ取り早いのは、各袋に入っているアイテム(Itemオブジェクト)を1つずつ取り出して、その売値を調べていく方法です。そのためには、ルートに当たるToolBoxオブジェクトからGetChild関数を通して登録されているToolオブジェクトに次々とアクセスしてGetName関数を呼び出します。もし袋だったらさらに中に入り、GetName関数をさらに呼び出します。もしアイテムだったらその名前から売値を参照するようにすればOKです。

 ところで、この「アイテム検索」は誰が行うのでしょうか?上の手っ取り早い方法だと、これはクライアントのお仕事のようです。しかし、正直めんどくさい。検索はとにかくめんどうなのです。

「あ〜あ、だれかアイテム取ってきてくれないかな」

これです!この役を買ってくれるのがVistor(訪問者)なんです。

 VisitorはToolクラスやItemクラス、Boxクラスのことを良く知っていて、もちろんそれがCompositeパターンであることも熟知していて、クライアントの変わりにアイテムを調べてきてくれるのです。

しかも、Toolクラス群は、「ようこそいらっしゃいました!」とVisitorを受け入れる関数まで用意してくれるのです。


 Toolクラス群にAccept関数が追加されました。これが「ようこそ」とVisitorを受け入れる関数です。この関数は仮想関数です(これが重要!)。

 VisitorクラスはToolクラス群の仮想関数であるAccept関数によって招き入れられます。やぁやぁと入ったVisitorは中で何をするかというと、Visitorクラスの持つ派生クラス専用の実行関数を実行してもらうのです。


 ItemOperation関数はItemクラスのAccept関数内で、BoxOperation関数はBoxクラスのAccept関数内で実行してもらいます。それぞれの関数は関数名に見合うクラスのポインタを引数にとっています。つまり、Toolクラス群のオブジェクトは訪問者であるVisitorに自分の情報を提供しているのです。しかも、自分がどのクラスなのかは「実行する関数の名前」とその引数で完全に明らかにされます。
 例えば、ItemクラスのAccept関数では、

Item::Accept(Visitor* visit)
{
   visit->ItemOperation(this);
}

とVisitorが用意したItemクラス専用関数を実行します。もちろん、BoxクラスのAccept関数ではvisit->BoxOperation(this)とするわけです。

 Visitorにしてみたら、ItemOperation関数の引数にあるのは「Itemオブジェクト」であることが分かっている事になるので、Itemオブジェクトから必要な情報をありがたく頂戴します。一方、BoxOperation関数の引数は「Boxオブジェクト」なので、アイテムではないことはわかっているため、BoxクラスのGetChild関数を実行してBoxの中に入っている「物」にまた自分を受け入れてもらいます。こうすることで、Boxの中にどんどん入っていって、アイテムがあったらその情報を得るという再帰的な検索が可能になるのです。

 どうでしょうか?複雑な振る舞いですよね。なぜこのようなことをしなければならないのか?それは、兎にも角にもクライアントが楽をしたいからです。データ構造はあくまでもデータです。複雑なToolデータ構造に「売値はいくらかね?」と聞くのはあまりに難しすぎます。そこで構造を解析して売値を調べるのですが、検索は面倒。だから、Visitorクラスに構造内に入ってもらって、代わりに売値計算をやってきてもらうわけです。肝心の「売値計算」はItemOperaion関数やBoxOperation関数に全部書いてあります。クライアントは、Visitorにさんざん検索してきてもらってから、「で、結果はどうでした?」と聞くだけです。



A おそるべしVisitorの力

 兎にも角にもVisitorクラスのItemOperation関数やBoxOperation関数にやりたいことを書けば、勝手に検索してきてくれることが分かりました。売値計算をするSoldVisitorクラスでは、ItemOperation関数内に引数のアイテムに相当する値段を計算させればよいのです。

 では、アイテムのリストを作ってきて欲しい場合はどうでしょう。これも実に簡単で、ListVisitor派生クラスを作り、ItemOperation関数内で引数のアイテムをリストに登録していけばよいのです。特定のアイテムがいくつあるかを検索する場合は?ItemOperation関数内で引数のアイテムを1つ1つチェックして、ひっかかればカウントすればよいのです。
 仮想関数であるItemOperation関数内からは、Visitorクラスの派生クラスで定義されたメンバ関数やメンバ変数は呼び放題ですから、必要な情報をいくらでも保持して持って帰って来れるのです。クライアントは、戻ってきたVsitorオブジェクトから結果を得るだけなのです。Visitorパターンは恐ろしく便利なのです!


 しかしながら、弱点もあります。Toolクラス群に新しくSpecialItemクラスを作ったとしましょう。すると、Visitorクラス内にこのクラスのAccept関数内で実行してもらうSpecialItemOperation関数を新しく設ける必要があります。しかし、その関数はVisitorクラスの派生クラス全部に波及してしまいますから、派生クラスで関数を再定義する必要があります。この手間が馬鹿になりません。よって、このパターンを用いるときには、ある程度データ構造が固まっているCompositeである事が大切になります。

 最後に、Visitorパターンのクラス図を示しましょう。


最初これを見たときには私も「うひゃー!」っと思いましたが、アイテムボックスの例に照らし合わせると、これが実に分かりやすいのです。


 ObjectStructureというのはCompositeパターンのような構造を保持しているオブジェクトの事です。このクラスがVisitorオブジェクトの受け渡しの入り口だと思っていただければ結構です。

クライアントが書くプログラムは結局これだけです。

SoldVisitor oSoldVistor;

// アイテムを検索して売値を計算
// GetRoot関数でCompositeのルートオブジェクトを取得するのがポイント
oObjectStructure->GetRoot()->Accept(&oSoldVisitor);

// 売値を取得
int SoldValue = oSoldVisitor.GetSoldValue();

 GoFの23種類のデザインパターンの中で最大級のクラス図で示されるVisitorパターン。使いこなせばもうデータ構造は怖くありません!!