Visitor
一言でいうと
Visitor (ビジター、 訪問者) は、 振る舞いに関するデザインパターンの一つで、 アルゴリズムをその動作対象となるオブジェクトから切り離します。
問題
自分のチームが一つの見事なグラフとして構造化された地理情報を使うアプリを開発していることを想像してみてください。 グラフの各ノードの表現するものは、 市のような複雑なものかもしれませんし、 もっと粒度の細かい、 産業、 観光地などかもしれません。 ノードが表現する実在のものとの間に道路があれば、 ノードは繋がります。 内部的には、 ノードの種類ごとに、 それ専用のクラスがあり、 各ノードはオブジェクトです。
ある時点で、 グラフの XML 形式でのエクスポートの実装を任されました。 一見して簡単そうな仕事です。 それぞれのノード・クラスにエクスポート用メソッドを追加し、 再帰を利用してグラフの各ノードを巡りながら、 エクスポートのメソッドを実行するだけです。 この解決策は、 シンプルでエレガントです。 多相性のおかげで、 エクスポート・メソッドを呼ぶコードは、 ノードの具象クラスと結合していません。
不運なことに、 システム・アーキテクトが既存のノード・クラスの変更の許可を拒否しました。 彼が言うには、 コードはすでに本番稼働中なので、 あなたの変更の中にあるかもしれない不具合によって障害が起きるリスクを負いたくないそうです。
それに加えて、 XML エクスポートのコードをノード・クラスに入れるのは、 理にかなっていることかどうか、 彼には疑問です。 これらのクラスの主要任務は、 地理情報データと機能することです。 XML エクスポートは、 そこにはなじみません。
拒否の理由がもう一つありました。 この機能が実装された後になって、 マーケティングの部署から、 他の形式でのエクスポートを追加しろとか、 他のわけのわからない要求が上がって来る可能性が大です。 これにより、 繊細で壊れやすいクラスを再度変更することを強いられることになるでしょう。
解決策
Visitor パターンでは、 新規の振る舞いを既存のクラスに統合するのではなく、 ビジターと呼ばれる個別のクラスに置きます。 その振る舞いを実行したい元のオブジェクトはビジターの引数として渡されます。 オブジェクトに含まれる必要なデータをそのメソッドがアクセスできることが前提です。
さて、 ここで、 もし振る舞いが異なるクラスのオブジェクト上で実行可能だとしたらどうでしょうか? たとえば、 XML エクスポートの例では、 実際の実装はノード・クラスの間で多少異なると思われます。 したがって、 ビジター・クラスは、 メソッドを一つだけではなく、 いくつかのメソッドの組を定義するかもしれません。 それぞれのメソッドは異なる型の引数を下記のように取ります:
しかし、 これらのメソッドはいったいどう呼べばいいのでしょう? 特にグラフ全体が対象の場合は? これらのメソッドは異なるシグネチャーを持っているので、 多相性は利用できません。 与えられたオブジェクトのプロセスが可能な、 適切なビジター・メソッドを選ぶためには、 そのクラスをチェックする必要があります。 まるで悪夢のように聞こえますね?
メソッドの多重定義を使えばいいでしょ? と皆さんは思うかもしれません。 つまり、 パラメーターの組み合わせが異うメソッドに同じ名前をつけるわけです。 残念ながら、 仮にプログラミング言語がそれをサポートしていたとしても (Java や C# はサポートしています)、 我々の助けにはなりません。 ノード・オブジェクトの正確なクラスが前もってわからないので、 多重定義の仕組みを使って、 実行すべき正しいメソッドを判定することができないからです。 基底の Node
クラスのオブジェクトを取るメソッドが使われることになってしまいます。
でも、 Visitor パターンは、 この問題に次のように対処します。 二重ディスパッチという技を使用することにより、 入り組んだ条件文なしで、 オブジェクトに適切なメソッドを呼べるようにします。 クライアントに適切な版のメソッドを選ばせる代わりに、 その任務を、 ビジターに引数として渡されるオジェクトに任せてはどうでしょう? オブジェクトは自分のクラスのことは分かっていますから、 ビジターの適切なメソッドを選ぶことは、 難なくできます。 オブジェクトは、 ビジターを 「受け入れ」 (accept)、 どの訪問メソッドが実行されるべきかをビジターに伝えます。
白状します。 結局のところノード・クラスの変更は必要でした。 しかし、 変更は些細なもので、 再度のコード変更なしに、 更なる振る舞いの追加が可能となります。
さて、 全部のビジターにとっての共通インターフェースを抽出さえすれば、 既存の全ノードは、 アプリに導入されるいかなるビジターとでも対応できるようになります。 ノードに関する新規の振る舞いを導入する立場になった場合、 やるべきことは新規のビジター・クラスの実装だけです。
現実世界でのたとえ
新規顧客を獲得したいと思っている経験豊富な保険の外交員を想像してみてください。 彼は近所のすべてのビルを訪問し、 会う人ごとに保険を販売しようとします。 ビルに入っている組織の種類に応じて、 それに特化した保険を提供します:
- 住宅ならば、 医療保険を販売します。
- 銀行ならば、 盗難保険を販売します。
- 喫茶店ならば、 火災・洪水保険を販売します。
構造
-
ビジター (Visitor) インターフェースは、 オブジェクト構造の具象要素を引数として取る一連の訪問メソッドを宣言します。 多重定義をサポートする言語でプログラムが書かれている場合は、 これらのメソッドの名前は同じでかまいません。 しかしパラメータの種類は違わなければなりません。
-
それぞれの具象ビジター (Concrete Visitor) は、 異なる具体的要素クラスに合わせて調整され、 同じ動作をするいくつかのバージョンを実装しています。
-
要素 (Element) インターフェースは、 ビジターを受け入れる (accept) ためのメソッドを宣言します。 このメソッドは、 ビジター・インターフェースの型で宣言されたパラメータ一つを取る必要があります。
-
各具象要素 (Concrete Element) は、 ビジターを受け入れるためのメソッドを実装する必要があります。 このメソッドの目的は、 現在の要素クラスに対応する適切なビジター・メソッドを呼び出すようにすることです。 仮に基底要素クラスがこのメソッドを実装していても、 すべてのサブクラスは、 このメソッドを上書きし、 ビジター・オブジェクト上の適切なメソッドを呼び出す必要があります。
-
クライアント (Client) は通常は、 コレクションまたはその他の複雑なオブジェクト (たとえば、 コンポジットツリー) を表します。 通常、 クライアントは具象要素クラスをすべて認識していません。 そのコレクションのオブジェクトと抽象的なインターフェースを通してやりとりするからです。
擬似コード
この例では、 Visitor パターンにより、 幾何学的形状のクラス階層に XML エクスポートのサポートを追加します。
もし、 この例でなぜ accept
メソッドが必要なんだろうと思われた方。 私の記事ビジターと二重ディスパッチ がその疑問に答えます。
適応性
複雑なオブジェクト構造 (例: オブジェクト・ツリー) のすべての要素に対してある操作を実行する必要がある場合は、 Visitor を使用します。
Visitor パターンを使用すると、 一つの操作を異なるクラスのオブジェクトに対して実行することができます。 これは、 ビジター・オブジェクトが、 対象クラスに応じてやや異なる同一操作の実装をすることでかなえられます。
補助的な振る舞いのビジネス・ロジックを整理するために Visitor を使用します。
このパターンは、 アプリの主要クラスがその主要任務に集中できるようにします。 これは、 他の付帯的な振る舞いをビジター・クラスに抽出することにより行います。
ある振る舞いが、 クラス階層の中のあるクラスでは意味があるが、 他のクラスでは意味がない場合に、 このクラスを使用します。
この振る舞いを別個のビジター・クラスに抽出し、 訪問メソッドのうち関連クラスのオブジェクトを受け入れるものだけ実装し、 他は空のままにします。
実装方法
-
プログラム中の各具象要素クラスごとに、 「訪問」 メソッドの組からなるビジター・インターフェースを宣言します。
-
要素インターフェースを宣言します。 もし既存の要素クラスの階層があるのであれば、 抽象 「受け入れ」 (accept) メソッドを階層の基底クラスに追加します。 このメソッドは、 ビジター・オブジェクトを引数として取ります。
-
すべての具象要素クラスで、 受け入れメソッド(複数あるかもしれません) を実装します。 これらのメソッドは、 単に呼び出しを、 現要素のクラスと一致する、 渡された訪問オブジェクトの訪問メソッドの呼び出しに換えます。
-
要素クラスは、 ビジター・インターフェースを通してのみ、 ビジターと関わるべきです。 ビジターは、 逆に、 訪問メソッドのパラメーター型として参照されるすべての具象要素クラスを知っておく必要があります。
-
要素の階層の中で実装できない振る舞いの一つごとに、 新しい具象ビジター・クラスを作成し、 訪問メソッドを全部実装します。
ビジターが要素クラスの非公開メンバーにアクセスする必要がある状況に遭遇するかもしれません。 この場合、 要素のカプセル化の目標に反してこれらのフィールドやメソッドを公開するか、 ビジター・クラスを要素クラス中にネストすることができます。 後者は、 幸運にも使用しているプログラミング言語がクラスのネストをサポートする場合にのみ可能です。
-
クライアントはビジター・オブジェクトを複数作成し、 それらを要素の 「受け入れ」 メソッドに渡します。
長所と短所
- 開放閉鎖の原則。 異なるクラスのオブジェクトと動作可能な新規の振る舞いを、 これらのクラスの変更なしに導入可能。
- 単一責任の原則。 同じ振る舞いの複数のバージョンを同一クラスに移動可能。
- ビジター・オブジェクトは、 様々なオブジェクトと動作しながら、 有用な情報を蓄積可能。 これはオブジェクト・ツリーのような複雑なオブジェクト構造を探索し、 この構造の各オブジェクトにビジターを適用したい場合に便利。
- 要素階層にクラスを追加または削除するたびに、 すべてのビジターを更新する必要あり。
- ビジターは、 作業する対象要素の非公開フィールドとメソッドへのアクセスができない可能性あり。
他のパターンとの関係
追記
- どうして Visitor パターンを単純にメソッド多重定義で置き換えられないのだろうと、 困惑していますか? 厄介な詳細については、 私の記事、 ビジターと二重ディスパッチをご一読ください。