春のセール

Visitor

別名:ビジター

一言でいうと

Visitor ビジター 訪問者 振る舞いに関するデザインパターンの一つで アルゴリズムをその動作対象となるオブジェクトから切り離します

Visitor デザインパターン

問題

自分のチームが一つの見事なグラフとして構造化された地理情報を使うアプリを開発していることを想像してみてください グラフの各ノードの表現するものは 市のような複雑なものかもしれませんし もっと粒度の細かい 産業 観光地などかもしれません ノードが表現する実在のものとの間に道路があれば ノードは繋がります 内部的には ノードの種類ごとに それ専用のクラスがあり 各ノードはオブジェクトです

グラフの XML へのエクスポート

グラフの XML へのエクスポート

ある時点で グラフの XML 形式でのエクスポートの実装を任されました 一見して簡単そうな仕事です それぞれのノード・クラスにエクスポート用メソッドを追加し 再帰を利用してグラフの各ノードを巡りながら エクスポートのメソッドを実行するだけです この解決策は シンプルでエレガントです 多相性のおかげで エクスポート・メソッドを呼ぶコードは ノードの具象クラスと結合していません

不運なことに システム・アーキテクトが既存のノード・クラスの変更の許可を拒否しました 彼が言うには コードはすでに本番稼働中なので あなたの変更の中にあるかもしれない不具合によって障害が起きるリスクを負いたくないそうです

XML エクスポートのメソッドをすべてのノード・クラスに追加する必要があった

XML エクスポートのメソッドをすべてのノード・クラスに追加する必要があった 変更中に不具合がまぎれこむことによるアプリケーション全体の機能停止のリスクあり

それに加えて XML エクスポートのコードをノード・クラスに入れるのは 理にかなっていることかどうか 彼には疑問です これらのクラスの主要任務は 地理情報データと機能することです XML エクスポートは そこにはなじみません

拒否の理由がもう一つありました この機能が実装された後になって マーケティングの部署から 他の形式でのエクスポートを追加しろとか 他のわけのわからない要求が上がって来る可能性が大です これにより 繊細で壊れやすいクラスを再度変更することを強いられることになるでしょう

解決策

Visitor パターンでは 新規の振る舞いを既存のクラスに統合するのではなく と呼ばれる個別のクラスに置きます その振る舞いを実行したい元のオブジェクトはビジターの引数として渡されます オブジェクトに含まれる必要なデータをそのメソッドがアクセスできることが前提です

さて ここで もし振る舞いが異なるクラスのオブジェクト上で実行可能だとしたらどうでしょうか たとえば XML エクスポートの例では 実際の実装はノード・クラスの間で多少異なると思われます したがって ビジター・クラスは メソッドを一つだけではなく いくつかのメソッドの組を定義するかもしれません それぞれのメソッドは異なる型の引数を下記のように取ります

class ExportVisitor implements Visitor is
    method doForCity(City c) { …… }
    method doForIndustry(Industry f) { …… }
    method doForSightSeeing(SightSeeing ss) { …… }
    // ……

しかし これらのメソッドはいったいどう呼べばいいのでしょう 特にグラフ全体が対象の場合は これらのメソッドは異なるシグネチャーを持っているので 多相性は利用できません 与えられたオブジェクトのプロセスが可能な 適切なビジター・メソッドを選ぶためには そのクラスをチェックする必要があります まるで悪夢のように聞こえますね

foreach (Node node in graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node)
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node)
    // ……
}

メソッドの多重定義を使えばいいでしょ と皆さんは思うかもしれません つまり パラメーターの組み合わせが異うメソッドに同じ名前をつけるわけです 残念ながら 仮にプログラミング言語がそれをサポートしていたとしても Java や C# はサポートしています 我々の助けにはなりません ノード・オブジェクトの正確なクラスが前もってわからないので 多重定義の仕組みを使って 実行すべき正しいメソッドを判定することができないからです 基底の Node クラスのオブジェクトを取るメソッドが使われることになってしまいます

でも Visitor パターンは この問題に次のように対処します 二重ディスパッチという技を使用することにより 入り組んだ条件文なしで オブジェクトに適切なメソッドを呼べるようにします クライアントに適切な版のメソッドを選ばせる代わりに その任務を ビジターに引数として渡されるオジェクトに任せてはどうでしょう オブジェクトは自分のクラスのことは分かっていますから ビジターの適切なメソッドを選ぶことは 難なくできます オブジェクトは ビジターを 受け入れ accept どの訪問メソッドが実行されるべきかをビジターに伝えます

// クライアントのコード
foreach (Node node in graph)
    node.accept(exportVisitor)

// 市
class City is
    method accept(Visitor v) is
        v.doForCity(this)
    // ……

// 産業
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this)
    // ……

白状します 結局のところノード・クラスの変更は必要でした しかし 変更は些細なもので 再度のコード変更なしに 更なる振る舞いの追加が可能となります

さて 全部のビジターにとっての共通インターフェースを抽出さえすれば 既存の全ノードは アプリに導入されるいかなるビジターとでも対応できるようになります ノードに関する新規の振る舞いを導入する立場になった場合 やるべきことは新規のビジター・クラスの実装だけです

現実世界でのたとえ

保険代理店の営業担当者

優れた保険の外交員は 様々な種類の組織に異なるポリシーを提供する用意がいつでもできている

新規顧客を獲得したいと思っている経験豊富な保険の外交員を想像してみてください 彼は近所のすべてのビルを訪問し 会う人ごとに保険を販売しようとします ビルに入っている組織の種類に応じて それに特化した保険を提供します

  • 住宅ならば 医療保険を販売します
  • 銀行ならば 盗難保険を販売します
  • 喫茶店ならば 火災・洪水保険を販売します

構造

Visitor デザインパターンの構造Visitor デザインパターンの構造
  1. ビジター Visitor インターフェースは オブジェクト構造の具象要素を引数として取る一連の訪問メソッドを宣言します 多重定義をサポートする言語でプログラムが書かれている場合は これらのメソッドの名前は同じでかまいません しかしパラメータの種類は違わなければなりません

  2. それぞれの具象ビジター Concrete Visitor 異なる具体的要素クラスに合わせて調整され 同じ動作をするいくつかのバージョンを実装しています

  3. 要素 Element インターフェースは ビジターを受け入れる accept ためのメソッドを宣言します このメソッドは ビジター・インターフェースの型で宣言されたパラメータ一つを取る必要があります

  4. 具象要素 Concrete Element ビジターを受け入れるためのメソッドを実装する必要があります このメソッドの目的は 現在の要素クラスに対応する適切なビジター・メソッドを呼び出すようにすることです 仮に基底要素クラスがこのメソッドを実装していても すべてのサブクラスは このメソッドを上書きし ビジター・オブジェクト上の適切なメソッドを呼び出す必要があります

  5. クライアント Client は通常は コレクションまたはその他の複雑なオブジェクト たとえば コンポジットツリー を表します 通常 クライアントは具象要素クラスをすべて認識していません そのコレクションのオブジェクトと抽象的なインターフェースを通してやりとりするからです

擬似コード

この例では Visitor パターンにより 幾何学的形状のクラス階層に XML エクスポートのサポートを追加します

Visitor パターン例の構造

ビジター・オブジェクトを介して様々な種類のオブジェクトを XML 形式でエクスポート

// 要素のインターフェースは、accept メソッドを宣言。これは、基底ビジター・
// インターフェースを引数として取る。
interface Shape is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

// 各具象要素クラスは、accept メソッドを実装しなければならない。実装は、
// 要素のクラスに対応したビジターのメソッドを呼び出すようなものでなければ
// ならない。
class Dot implements Shape is
    // ……

    // ここでは現クラスを受け入れる visitDot を呼び出していることに注意。
    // このようにしてビジターに、動作の対象となる要素のクラスを知らせる。
    method accept(v: Visitor) is
        v.visitDot(this)

class Circle implements Shape is
    // ……
    method accept(v: Visitor) is
        v.visitCircle(this)

class Rectangle implements Shape is
    // ……
    method accept(v: Visitor) is
        v.visitRectangle(this)

class CompoundShape implements Shape is
    // ……
    method accept(v: Visitor) is
        v.visitCompoundShape(this)


// Visitor インターフェースは、要素のクラスに応じた訪問メソッドの集まりを
// 宣言。訪問メソッドのシグネチャーから、ビジターは処理対象の要素の正確な
// クラスを特定可能。
interface Visitor is
    method visitDot(d: Dot)
    method visitCircle(c: Circle)
    method visitRectangle(r: Rectangle)
    method visitCompoundShape(cs: CompoundShape)

// ビジターの具象クラスは、同じアルゴリズムのいくつか異なるバージョンを実
// 装し、全具象要素クラスと動作可能。
//
// Composite ツリー等、複雑なオブジェクト構造に対して用いると、Visitor
// パターンの利点を最大限に活用可能。この場合、構造中の種々のオブジェクト
// に対してビジター・メソッドの実行中に、アルゴリズムの中間状態を保存する
// ことが役に立つ場合あり。
class XMLExportVisitor implements Visitor is
    method visitDot(d: Dot) is
        // 点の ID と中心座標をエクスポート。

    method visitCircle(c: Circle) is
        // 円の ID と中心座標と半径をエクスポート。

    method visitRectangle(r: Rectangle) is
        // 長方形の ID と左上の座標と幅と高さをエクスポート。

    method visitCompoundShape(cs: CompoundShape) is
        // 形状の ID と子の ID のリストをエクスポート。


// クライアント・コードは、具象クラスを把握することなく、一連の要素に対し
// てビジター操作を実行可能。accept 操作は、ビジター・オブジェクト内の適
// 切な操作への呼び出しとなる。
class Application is
    field allShapes: array of Shapes

    method export() is
        exportVisitor = new XMLExportVisitor()

        foreach (shape in allShapes) do
            shape.accept(exportVisitor)

もし この例でなぜ accept メソッドが必要なんだろうと思われた方 私の記事ビジターと二重ディスパッチ がその疑問に答えます

適応性

複雑なオブジェクト構造 オブジェクト・ツリー のすべての要素に対してある操作を実行する必要がある場合は Visitor を使用します

Visitor パターンを使用すると 一つの操作を異なるクラスのオブジェクトに対して実行することができます これは ビジター・オブジェクトが 対象クラスに応じてやや異なる同一操作の実装をすることでかなえられます

補助的な振る舞いのビジネス・ロジックを整理するために Visitor を使用します

このパターンは アプリの主要クラスがその主要任務に集中できるようにします これは 他の付帯的な振る舞いをビジター・クラスに抽出することにより行います

ある振る舞いが クラス階層の中のあるクラスでは意味があるが 他のクラスでは意味がない場合に このクラスを使用します

この振る舞いを別個のビジター・クラスに抽出し 訪問メソッドのうち関連クラスのオブジェクトを受け入れるものだけ実装し 他は空のままにします

実装方法

  1. プログラム中の各具象要素クラスごとに 訪問 メソッドの組からなるビジター・インターフェースを宣言します

  2. 要素インターフェースを宣言します もし既存の要素クラスの階層があるのであれば 抽象 受け入れ accept メソッドを階層の基底クラスに追加します このメソッドは ビジター・オブジェクトを引数として取ります

  3. すべての具象要素クラスで 受け入れメソッド(複数あるかもしれません を実装します これらのメソッドは 単に呼び出しを 現要素のクラスと一致する 渡された訪問オブジェクトの訪問メソッドの呼び出しに換えます

  4. 要素クラスは ビジター・インターフェースを通してのみ ビジターと関わるべきです ビジターは 逆に 訪問メソッドのパラメーター型として参照されるすべての具象要素クラスを知っておく必要があります

  5. 要素の階層の中で実装できない振る舞いの一つごとに 新しい具象ビジター・クラスを作成し 訪問メソッドを全部実装します

    ビジターが要素クラスの非公開メンバーにアクセスする必要がある状況に遭遇するかもしれません この場合 要素のカプセル化の目標に反してこれらのフィールドやメソッドを公開するか ビジター・クラスを要素クラス中にネストすることができます 後者は 幸運にも使用しているプログラミング言語がクラスのネストをサポートする場合にのみ可能です

  6. クライアントはビジター・オブジェクトを複数作成し それらを要素の 受け入れ メソッドに渡します

長所と短所

  • 異なるクラスのオブジェクトと動作可能な新規の振る舞いを これらのクラスの変更なしに導入可能
  • 同じ振る舞いの複数のバージョンを同一クラスに移動可能
  • ビジター・オブジェクトは 様々なオブジェクトと動作しながら 有用な情報を蓄積可能 これはオブジェクト・ツリーのような複雑なオブジェクト構造を探索し この構造の各オブジェクトにビジターを適用したい場合に便利
  • 要素階層にクラスを追加または削除するたびに すべてのビジターを更新する必要あり
  • ビジターは 作業する対象要素の非公開フィールドとメソッドへのアクセスができない可能性あり

他のパターンとの関係

  • VisitorCommand パターンのより強力なものとして扱うことができます そのオブジェクトは 異なるクラスの様々なオブジェクトに対して操作を実行できます

  • Visitor を使用して Composite ツリー全体に対して一つの操作を実行できます

  • 複雑なデータ構造を探索し その要素に対してある操作を実行するために VisitorIterator と一緒に使用することができます 要素のクラスが全部異なっていてもかまいません

コード例

Visitor を C# で Visitor を C++ で Visitor を Go で Visitor を Java で Visitor を PHP で Visitor を Python で Visitor を Ruby で Visitor を Rust で Visitor を Swift で Visitor を TypeScript で

追記

  • どうして Visitor パターンを単純にメソッド多重定義で置き換えられないのだろうと 困惑していますか 厄介な詳細については 私の記事 ビジターと二重ディスパッチをご一読ください