web-dev-qa-db-ja.com

訪問者デザインパターンはいつ使用する必要がありますか?

私はブログで訪問者パターンへの参照を見続けていますが、私は認めなければなりません、私はそれを得ません。 パターンのウィキペディアの記事 を読んで、その仕組みを理解していますが、それをいつ使用するかについてはまだ混乱しています。

最近デコレータパターンを本当にgotし、どこでも絶対にそれを使用している人として、これを一見直感的に本当に理解できるようにしたい便利なパターンも。

301
George Mauer

私は訪問者パターンにあまり詳しくありません。私が正解したかどうか見てみましょう。動物の階層があるとします

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(確立されたインターフェースを持つ複雑な階層であると仮定します。)

次に、階層に新しい操作を追加します。つまり、各動物に音を出させます。階層がこれほど単純である限り、ストレートポリモーフィズムでそれを行うことができます。

class Animal
{ public: virtual void makeSound() = 0; };

class Dog : public Animal
{ public: void makeSound(); };

void Dog::makeSound()
{ std::cout << "woof!\n"; }

class Cat : public Animal
{ public: void makeSound(); };

void Cat::makeSound()
{ std::cout << "meow!\n"; }

ただし、この方法で処理を進める場合は、操作を追加するたびに、階層の各クラスへのインターフェイスを変更する必要があります。今、代わりに、元のインターフェイスに満足し、それに対して可能な限り少ない変更を加えたいとします。

訪問者パターンを使用すると、適切なクラスで新しい各操作を移動でき、階層のインターフェースを1回だけ拡張する必要があります。やってみましょう。まず、抽象操作( GoF の「Visitor」クラス)を定義します。これには、階層内のすべてのクラスのメソッドがあります。

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

次に、新しい操作を受け入れるために階層を変更します。

class Animal
{ public: virtual void letsDo(Operation *v) = 0; };

class Dog : public Animal
{ public: void letsDo(Operation *v); };

void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }

class Cat : public Animal
{ public: void letsDo(Operation *v); };

void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }

最後に、CatもDogも変更せずに、実際の操作を実装します

class Sound : public Operation
{
public:
    void hereIsADog(Dog *d);
    void hereIsACat(Cat *c);
};

void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }

void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }

これで、階層を変更しなくても操作を追加できます。仕組みは次のとおりです。

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}
298

あなたの混乱の理由は、おそらく訪問者が致命的な誤名であるためです。多くの(著名な1!)プログラマーはこの問題につまずいた。実際に行うことは、ネイティブにサポートしていない言語(ほとんどの言語ではサポートしていない言語)で double dispatching を実装することです。


1) 私のお気に入りの例はスコット・マイヤーズです。「Effective C++」の称賛を受けた著者で、これを彼の 最も重要なC++ aha!momentseverと呼びました)

124
Konrad Rudolph

ここの誰もが正しいですが、私はそれが「いつ」に対処できないと思います。まず、デザインパターンから:

訪問者は、操作対象の要素のクラスを変更せずに新しい操作を定義できます。

ここで、単純なクラス階層について考えてみましょう。クラス1、2、3、4とメソッドA、B、C、Dがあります。スプレッドシートのようにレイアウトします。クラスは行で、メソッドは列です。

現在、オブジェクト指向設計では、新しいメソッドよりも新しいクラスを成長させる可能性が高いため、いわば行を追加する方が簡単です。新しいクラスを追加し、そのクラスの違いを指定し、残りを継承するだけです。

ただし、クラスは比較的静的な場合もありますが、より多くのメソッドを追加する必要があります(列の追加)。 OO設計の標準的な方法は、すべてのクラスにこのようなメソッドを追加することです。これはコストがかかる可能性があります。 Visitorパターンはこれを簡単にします。

ところで、これはScalaのパターンマッチが解決しようとしている問題です。

81

Visitor設計パターンは、ディレクトリツリー、XML構造、ドキュメントアウトラインなどの「再帰的」構造に非常に適しています。

Visitorオブジェクトは、再帰構造内の各ノード(各ディレクトリ、各XMLタグなど)にアクセスします。 Visitorオブジェクトは、構造をループしません。代わりに、Visitorメソッドが構造の各ノードに適用されます。

典型的な再帰ノード構造は次のとおりです。ディレクトリまたはXMLタグの可能性があります。 [Javaの人なら、子リストを作成および管理するための追加のメソッドがたくさんあると想像してください。]

class TreeNode( object ):
    def __init__( self, name, *children ):
        self.name= name
        self.children= children
    def visit( self, someVisitor ):
        someVisitor.arrivedAt( self )
        someVisitor.down()
        for c in self.children:
            c.visit( someVisitor )
        someVisitor.up()

visitメソッドは、構造内の各ノードに訪問者オブジェクトを適用します。この場合、トップダウンの訪問者です。 visitメソッドの構造を変更して、ボトムアップまたはその他の順序付けを行うことができます。

これが訪問者向けのスーパークラスです。 visitメソッドによって使用されます。構造内の各ノードに「到着」します。 visitメソッドはupおよびdownを呼び出すため、訪問者は深さを追跡できます。

class Visitor( object ):
    def __init__( self ):
        self.depth= 0
    def down( self ):
        self.depth += 1
    def up( self ):
        self.depth -= 1
    def arrivedAt( self, aTreeNode ):
        print self.depth, aTreeNode.name

サブクラスは、各レベルでノードをカウントし、ノードのリストを蓄積して、Niceパスの階層セクション番号を生成するなどの処理を実行できます。

これがアプリケーションです。ツリー構造someTreeを構築します。 VisitordumpNodesを作成します。

次に、dumpNodesをツリーに適用します。 dumpNodeオブジェクトは、ツリー内の各ノードを「訪問」します。

someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )

TreeNode visitアルゴリズムは、すべてのTreeNodeが訪問者のarrivedAtメソッドへの引数として使用されることを保証します。

22
S.Lott

それを見る1つの方法は、ビジターパターンが、特定のクラス階層内のすべてのクラスにクライアントが追加のメソッドを追加できるようにする方法であるということです。

かなり安定したクラス階層がある場合に役立ちますが、その階層で何をする必要があるかについての要件が変わります。

古典的な例は、コンパイラなどです。抽象構文ツリー(AST)はプログラミング言語の構造を正確に定義できますが、ASTで実行する操作は、プロジェクトの進行に応じて変更されます:コードジェネレーター、プリティプリンター、デバッガー、複雑さの指標分析。

訪問者パターンがないと、開発者が新しい機能を追加するたびに、そのメソッドを基本クラスのすべての機能に追加する必要があります。これは、基本クラスが別のライブラリに表示される場合、または別のチームによって作成される場合、特に困難です。

(Visitorパターンは、データの操作をデータから遠ざけるため、良いOO慣行と矛盾していると主張したと聞きました。Visitorパターンは、通常のOOプラクティスは失敗します。)

18
Oddthinking

訪問者パターンを使用する理由は少なくとも3つあります。

  1. データ構造が変更されたときにわずかに異なるコードの拡散を減らします。

  2. 計算を実装するコードを変更せずに、同じ計算を複数のデータ構造に適用します。

  3. レガシーコードを変更せずに、レガシーライブラリに情報を追加します。

これについて書いた記事 をご覧ください。

14
Richard Gomes

Konrad Rudolphがすでに指摘したように、double dispatchが必要な場合に適しています

以下に、二重ディスパッチが必要な状況と、訪問者がそれを行う方法を示します。

例:

IPhone、Android、Windows Mobileの3種類のモバイルデバイスがあるとします。

これら3つのデバイスにはすべて、Bluetooth無線がインストールされています。

Bluetoothラジオは、IntelとBroadcomの2つの個別のOEMからのものであると想定しましょう。

議論に関連する例を作るために、Intelラジオで公開されているAPIがBroadcomラジオで公開されているAPIとは異なると仮定してみましょう。

これが私のクラスの見た目です–

enter image description hereenter image description here

次に、モバイルデバイスでBluetoothをオンにする操作を紹介します。

関数のシグネチャは次のようになります。

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

したがって、デバイスの正しいタイプおよびBluetooth無線の正しいタイプに依存適切なステップまたはアルゴリズムを呼び出すことでスイッチを入れることができます

原則として、それは3 x 2のマトリックスになります。ここで、関連するオブジェクトの適切なタイプに応じて、適切な操作をベクトル化しようとしています。

両方の引数のタイプに応じた多態的な動作。

enter image description here

これで、訪問者パターンをこの問題に適用できます。インスピレーションは、ウィキペディアのページに記載されています-「本質的に、訪問者はクラス自体を変更せずにクラスのファミリーに新しい仮想機能を追加できます。代わりに、仮想関数のすべての適切な専門化を実装するビジタークラスを作成します。訪問者はインスタンス参照を入力として受け取り、ダブルディスパッチにより目標を実装します。」

3x2マトリックスのため、ここではダブルディスパッチが必要です

セットアップは次のようになります。- enter image description here

私は別の質問に答えるために例を書いた。コードとその説明は ここ である。

13
Kapoor

次のリンクで簡単に見つけました:

http://www.remondo.net/visitor-pattern-example-csharp/ で、訪問者パターンの利点を示す模擬例を示す例を見つけました。ここには、Pillのさまざまなコンテナクラスがあります。

namespace DesignPatterns
{
    public class BlisterPack
    {
        // Pairs so x2
        public int TabletPairs { get; set; }
    }

    public class Bottle
    {
        // Unsigned
        public uint Items { get; set; }
    }

    public class Jar
    {
        // Signed
        public int Pieces { get; set; }
    }
}

上記でわかるように、BilsterPackにはピルのペアが含まれているため、ペアの数に2を掛ける必要があります。また、Bottleは異なるデータ型でキャストする必要があるunitを使用することに気付くかもしれません。

したがって、メインメソッドでは、次のコードを使用してピル数を計算できます。

foreach (var item in packageList)
{
    if (item.GetType() == typeof (BlisterPack))
    {
        pillCount += ((BlisterPack) item).TabletPairs * 2;
    }
    else if (item.GetType() == typeof (Bottle))
    {
        pillCount += (int) ((Bottle) item).Items;
    }
    else if (item.GetType() == typeof (Jar))
    {
        pillCount += ((Jar) item).Pieces;
    }
}

上記のコードはSingle Responsibility Principleに違反していることに注意してください。つまり、新しいタイプのコンテナを追加する場合は、メインメソッドコードを変更する必要があります。また、切り替えを長くすることは悪い習慣です。

そのため、次のコードを導入します。

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int)bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

Pillsの数をカウントする責任をPillCountVisitorというクラスに移動しました(さらにswitch caseステートメントを削除しました)。つまり、新しいタイプのピルコンテナを追加する必要がある場合は必ず、PillCountVisitorクラスのみを変更する必要があります。また、IVisitorインターフェイスは、別のシナリオで使用するために一般的であることに注意してください。

Acceptメソッドをpillコンテナークラスに追加することにより:

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

訪問者が丸薬容器クラスを訪問できるようにします。

最後に、次のコードを使用して錠剤数を計算します。

var visitor = new PillCountVisitor();

foreach (IAcceptor item in packageList)
{
    item.Accept(visitor);
}

つまり、すべてのピルコンテナにより、PillCountVisitor訪問者がピル数を確認できます。彼はあなたの薬を数える方法を知っています。

visitor.Countには錠剤の価値があります。

http://butunclebob.com/ArticleS.UncleBob.IuseVisitor では、単一の責任原則に従うために ポリモーフィズム (答え)を使用できない実際のシナリオが表示されます。実際には:

public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

reportQtdHoursAndPayメソッドは、レポートと表現のためのものであり、これは単一責任原則に違反します。そのため、ビジターパターンを使用して問題を解決することをお勧めします。

ダブルディスパッチはこのパターンを使用する理由の1つです。
ただし、単一のディスパッチパラダイムを使用する言語で2つ以上のディスパッチを実装するのは単一の方法です。

パターンを使用する理由は次のとおりです。

1)毎回モデルを変更せずに新しいオペレーションを定義するモデルは頻繁に変更されますが、オペレーションは頻繁に変更されます。

2)モデルと動作を組み合わせたくない複数のアプリケーションで再利用可能なモデルが必要または拡張可能なモデルが必要クライアントクラスが独自のクラスで動作を定義できるようにします。

3)モデルの具体的なタイプに依存する一般的な操作がありますが、複数のクラスで複数の場所で共通のロジックを爆発させるため、各サブクラスでロジックを実装したくない

4)ドメインモデルの設計を使用しており、同じ階層のモデルクラスは、他の場所に収集される可能性のある非常に多くの異なる処理を実行します

5)二重ディスパッチが必要
インターフェイスタイプで宣言された変数があり、それらをランタイムタイプに応じて処理できるようにしたいのですが、もちろんif (myObj instanceof Foo) {}またはトリックを使用せずに。
たとえば、これらの変数を、特定の処理を適用するためのパラメーターとしてインターフェイスの具体的なタイプを宣言するメソッドに渡すことです。実行時に呼び出される選択はレシーバーのランタイムタイプのみに依存するため、この方法は、言語ではすぐに使用できるシングルディスパッチに依存しません。
Javaでは、呼び出すメソッド(シグネチャ)はコンパイル時に選択され、ランタイムタイプではなく、宣言されたパラメーターのタイプに依存することに注意してください。

ビジターを使用する理由である最後の点も結果です。ビジターを実装すると(もちろん、複数のディスパッチをサポートしない言語の場合)、必ずダブルディスパッチの実装を導入する必要があります。

各要素に訪問者を適用するための要素の走査(反復)は、パターンを使用する理由ではないことに注意してください。
モデルと処理を分割するため、パターンを使用します。
そして、パターンを使用することで、イテレーター機能の利点も得られます。
この機能は非常に強力であり、accept()は汎用メソッドであるため、特定のメソッドを使用した一般的な型の反復を超えています。
これは特別な使用例です。それで、私はそれを片側に置きます。


Javaの例

チェスの例でパターンの付加価値を説明します。この例では、プレーヤーが駒の移動を要求したときの処理を定義します。

ビジターパターンを使用しないと、ピースのサブクラスでピースの移動動作を直接定義できます。
たとえば、次のようなPieceインターフェイスを使用できます。

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

各Pieceサブクラスは、次のように実装します。

public class Pawn implements Piece{

    @Override
    public boolean checkMoveValidity(Coordinates coord) {
        ...
    }

    @Override
    public void performMove(Coordinates coord) {
        ...
    }

    @Override
    public Piece computeIfKingCheck() {
        ...
    }

}

そして、すべてのPieceサブクラスにも同じことが言えます。
これは、この設計を示すダイアグラムクラスです。

[model class diagram

このアプローチには、3つの重要な欠点があります。

performMove()computeIfKingCheck()などの動作は、おそらく一般的なロジックを使用します。
たとえば、具体的なPieceperformMove()は最終的に現在のピースを特定の場所に設定し、潜在的に相手のピースを取得します。
複数のクラスに関連する動作を収集するのではなく分割すると、何らかの形で単一の責任パターンが無効になります。保守性をより難しくします。

checkMoveValidity()としての処理は、Pieceサブクラスが参照または変更するものであってはなりません。
それは、人間やコンピューターの行動を超えたチェックです。このチェックは、プレーヤーが要求した各アクションで実行され、要求されたピースの移動が有効であることを確認します。
だから、Pieceインターフェースでそれを提供したくはありません。

–ボット開発者にとって困難なチェスゲームでは、通常、アプリケーションは標準API(Pieceインターフェイス、サブクラス、ボード、一般的な動作など)を提供し、開発者にボット戦略を充実させます。
それを可能にするには、Piece実装でデータと動作が密結合していないモデルを提案する必要があります。

それでは、ビジターパターンを使用してみましょう。

次の2種類の構造があります。

–訪問を受け入れるモデルクラス(ピース)

–訪問者(移動操作)

パターンを示すクラス図は次のとおりです。

enter image description here

上部には訪問者がおり、下部にはモデルクラスがあります。

以下はPieceMovingVisitorインターフェイスです(Pieceの種類ごとに指定された動作):

public interface PieceMovingVisitor {

    void visitPawn(Pawn pawn);

    void visitKing(King king);

    void visitQueen(Queen queen);

    void visitKnight(Knight knight);

    void visitRook(Rook rook);

    void visitBishop(Bishop bishop);

}

ピースが定義されました:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

その主要な方法は次のとおりです。

void accept(PieceMovingVisitor pieceVisitor);

Pieceレシーバーに基づいた呼び出しである最初のディスパッチを提供します。
コンパイル時に、メソッドはPieceインターフェースのaccept()メソッドにバインドされ、実行時に、バインドされたメソッドはランタイムPieceクラスで呼び出されます。
そして、2番目のディスパッチを実行するのはaccept()メソッドの実装です。

実際、Pieceオブジェクトがアクセスしたい各PieceMovingVisitorサブクラスは、引数自体を渡すことでPieceMovingVisitor.visit()メソッドを呼び出します。
このようにして、コンパイラーは、コンパイル時に、宣言されたパラメーターの型を具象型にバインドします。
2回目のディスパッチがあります。
これは、以下を示すBishopサブクラスです。

public class Bishop implements Piece {

    private Coordinates coord;

    public Bishop(Coordinates coord) {
        super(coord);
    }

    @Override
    public void accept(PieceMovingVisitor pieceVisitor) {
        pieceVisitor.visitBishop(this);
    }

    @Override
    public Coordinates getCoordinates() {
        return coordinates;
    }

   @Override
    public void setCoordinates(Coordinates coordinates) {
        this.coordinates = coordinates;
   }

}

そして、ここで使用例:

// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();

// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);

// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
    piece.accept(new MovePerformingVisitor(coord));
}

訪問者の欠点

Visitorパターンは非常に強力なパターンですが、使用する前に考慮すべき重要な制限もいくつかあります。

1)カプセル化を低減/破壊するリスク

ある種の操作では、ビジターパターンがドメインオブジェクトのカプセル化を削減または破壊する場合があります。

たとえば、MovePerformingVisitorクラスは実際のピースの座標を設定する必要があるため、Pieceインターフェイスはそれを行う方法を提供する必要があります。

void setCoordinates(Coordinates coordinates);

Piece座標の変更の責任は、Pieceサブクラス以外の他のクラスに開かれています。
ビジターが実行する処理をPieceサブクラスに移動することもオプションではありません。
Piece.accept()は訪問者の実装を受け入れるため、実際に別の問題が発生します。訪問者が何を実行するのかがわからないため、ピースの状態を変更するかどうか、および変更する方法についてはわかりません。
ビジターを識別する方法は、ビジターの実装に従ってPiece.accept()で後処理を実行することです。それは、Visitor実装とPieceサブクラスの間に高い結合を作成し、さらにおそらくgetClass()instanceof、またはVisitor実装を識別するマーカーとしてトリックを使用する必要があるため、非常に悪い考えです。

2)モデルを変更するための要件

たとえば、Decoratorなどの他のビヘイビアデザインパターンとは異なり、訪問者パターンは侵入的です。
訪問することを受け入れるaccept()メソッドを提供するために、最初のレシーバークラスを変更する必要があります。
Pieceとそのサブクラスはourクラスであるため、問題はありませんでした。
ビルトインまたはサードパーティのクラスでは、物事はそれほど簡単ではありません。
accept()メソッドを追加するには、それらをラップまたは継承する(可能な場合)必要があります。

)インダイレクション

このパターンは、複数のインダイレクションを作成します。
ダブルディスパッチとは、1回ではなく2回の呼び出しを意味します。

call the visited (piece) -> that calls the visitor (pieceMovingVisitor)

また、訪問者が訪問済みオブジェクトの状態を変更するときに、追加の間接性を設定できます。
それはサイクルのように見えるかもしれません:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)
5
davidxxx

Cay Horstmannには、適用先の素晴らしい例があります 彼のOOデザインとパターンの本の訪問者 。彼は問題を要約します:

複合オブジェクトは、多くの場合、個々の要素で構成される複雑な構造を持っています。一部の要素には再び子要素が含まれる場合があります。 ...要素の操作は、その子要素を訪問し、それらに操作を適用し、結果を結合します。 ...しかし、そのような設計に新しい操作を追加するのは簡単ではありません。

簡単ではない理由は、構造クラス自体に操作が追加されるためです。たとえば、ファイルシステムがあるとします。

FileSystem class diagram

この構造を使用して実装する必要がある操作(機能)は次のとおりです。

  • ノード要素の名前を表示する(ファイルリスト)
  • ノード要素の計算されたサイズを表示します(ディレクトリのサイズには、そのすべての子要素のサイズが含まれます)
  • 等.

FileSystemの各クラスに関数を追加して、操作を実装することができます(これを行う方法は非常に明白なので、人々はこれを過去に行っています)。問題は、新しい機能(上記の "etc."行)を追加するたびに、構造クラスにメソッドを追加する必要がある場合があることです。ある時点で、ソフトウェアにいくつかの操作を追加した後、それらのクラスのメソッドは、クラスの機能的凝集という観点からは意味をなしません。たとえば、ファイルシステムに最新の視覚化機能を実装するために、calculateFileColorForFunctionABC()メソッドを持つFileNodeがあります。

ビジターパターン(多くのデザインパターンと同様)は、苦痛と苦しみから生まれました。どこでも多くの変更を必要とし、優れた設計原則(高い凝集度、低い結合)を尊重します。あなたがその痛みを感じるまで、多くのパターンの有用性を理解することは難しいと思う。痛みの説明(追加される「など」の機能を使用して上記のことを試みるなど)は、説明のスペースを取り、気を散らすものです。このため、パターンを理解することは困難です。

訪問者は、データ構造自体の機能からデータ構造の機能(たとえば、FileSystemNodes)を分離することができます。このパターンにより、設計は結束を尊重することができます。データ構造クラスはより単純で(メソッドが少ない)、機能もVisitor実装にカプセル化されます。これは、double-dispatching(パターンの複雑な部分)を介して行われます:構造クラスでaccept()メソッドを使用し、visitX() Visitor(機能)クラスのメソッド:

FileSystem class diagram with Visitor applied

この構造により、構造に具体的な訪問者として機能する新しい機能を追加できます(構造クラスを変更することなく)。

FileSystem class diagram with Visitor applied

たとえば、ディレクトリ一覧機能を実装するPrintNameVisitorと、サイズ付きのバージョンを実装するPrintSizeVisitor。 XMLでデータを生成する 'ExportXMLVisitor`、またはJSONでデータを生成する別のビジターなどがある日を想像できます。 DOTなどのグラフィカル言語を使用して、ディレクトリツリーを表示するビジターもあります 、別のプログラムで視覚化されます。

最後の注意点として、ダブルディスパッチによる訪問者の複雑さは、理解、コーディング、デバッグがより困難であることを意味します。要するに、オタク要素が高く、KISS原則に反します。 研究者による調査では、Visitorは議論の余地のあるパターンであることが示されました(その有用性についてコンセンサスはありませんでした)。いくつかの実験では、コードの保守が容易にならないことが示されました。

5
Fuhrmanator

Aspect Objectプログラミングと同じアンダーグラウンド実装としての訪問者パターン。

たとえば、操作対象の要素のクラスを変更せずに新しい操作を定義する場合

5
mixturez

私の意見では、新しい操作を追加する作業量は、Visitor Patternまたは各要素構造の直接変更を使用してほぼ同じです。また、Cowなどの新しい要素クラスを追加すると、Operationインターフェイスが影響を受け、これが既存の要素のすべてのクラスに伝播するため、すべての要素クラスの再コンパイルが必要になります。それで、ポイントは何ですか?

5
kaosad

訪問者パターンの簡単な説明。変更が必要なクラスはすべて、「accept」メソッドを実装する必要があります。クライアントは、このacceptメソッドを呼び出して、そのクラスのファミリに対して新しいアクションを実行し、機能を拡張します。クライアントは、この1つのacceptメソッドを使用して、特定のアクションごとに異なるビジタークラスを渡すことにより、幅広い新しいアクションを実行できます。訪問者クラスには、ファミリ内のすべてのクラスに対して同じ特定のアクションを達成する方法を定義する複数のオーバーライドされた訪問メソッドが含まれます。これらの訪問メソッドには、動作するインスタンスが渡されます。

使用を検討する場合

  1. クラスのファミリがある場合、それらのすべてに多くの新しいアクションを追加する必要があることを知っていますが、何らかの理由で将来クラスのファミリを変更または再コンパイルすることはできません。
  2. 新しいアクションを追加し、その新しいアクションを複数のクラスに分散するのではなく、ビジタークラス内で完全に定義する場合。
  3. 上司が何かをしなければならないクラスの範囲を作成する必要があると言ったとき!...
5
andrew pate

訪問者

訪問者は、クラス自体を変更せずにクラスのファミリーに新しい仮想関数を追加できます。代わりに、仮想関数の適切な特殊化をすべて実装するビジタークラスを作成します

訪問者の構造:

enter image description here

次の場合に訪問者パターンを使用:

  1. 同様の操作を実行する必要があります
  2. 多くの別個の無関係な操作を実行する必要があります。 オブジェクトから操作を分離します構造
  3. オブジェクト構造を変更せずに新しい操作を追加する必要があります
  4. クラスの変更や派生を強制するのではなく、関連する操作を単一のクラスに集めます
  5. あなたがソースを持っていないか、ソースを変更できないクラスライブラリに関数を追加します

Visitorパターンは、Objectの既存のコードを変更せずに新しい操作を追加する柔軟性を提供しますが、この柔軟性には欠点があります。

新しいVisitableオブジェクトが追加された場合、VisitorおよびConcreteVisitorクラスのコード変更が必要です。この問題に対処する回避策があります:パフォーマンスに影響を与えるリフレクションを使用してください。

コードスニペット:

import Java.util.HashMap;

interface Visitable{
    void accept(Visitor visitor);
}

interface Visitor{
    void logGameStatistics(Chess chess);
    void logGameStatistics(Checkers checkers);
    void logGameStatistics(Ludo ludo);    
}
class GameVisitor implements Visitor{
    public void logGameStatistics(Chess chess){
        System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");    
    }
    public void logGameStatistics(Checkers checkers){
        System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");    
    }
    public void logGameStatistics(Ludo ludo){
        System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");    
    }
}

abstract class Game{
    // Add game related attributes and methods here
    public Game(){

    }
    public void getNextMove(){};
    public void makeNextMove(){}
    public abstract String getName();
}
class Chess extends Game implements Visitable{
    public String getName(){
        return Chess.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Checkers extends Game implements Visitable{
    public String getName(){
        return Checkers.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Ludo extends Game implements Visitable{
    public String getName(){
        return Ludo.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}

public class VisitorPattern{
    public static void main(String args[]){
        Visitor visitor = new GameVisitor();
        Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
        for (Visitable v : games){
            v.accept(visitor);
        }
    }
}

説明:

  1. VisitableElement)はインターフェイスであり、このインターフェイスメソッドをクラスのセットに追加する必要があります。
  2. Visitorは、Visitable要素で操作を実行するメソッドを含むインターフェイスです。
  3. GameVisitorは、Visitorインターフェイス(ConcreteVisitor)を実装するクラスです。
  4. Visitable要素はVisitorを受け入れ、Visitorインターフェイスの関連メソッドを呼び出します。
  5. GameElementとして扱い、Chess,Checkers and Ludoのような具体的なゲームをConcreteElementsとして扱うことができます。

上記の例では、Chess, Checkers and Ludoは3つの異なるゲーム(およびVisitableクラス)です。ある晴れた日に、各ゲームの統計を記録するシナリオに遭遇しました。したがって、個々のクラスを変更して統計機能を実装することなく、その責任をGameVisitorクラスに集中化できます。これにより、各ゲームの構造を変更することなくトリックを実行できます。

出力:

Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser

参照する

oodesignの記事

ソース作成 記事

詳細については

デコレーター

パターンを使用すると、同じクラスの他のオブジェクトの動作に影響を与えることなく、静的または動的に個々のオブジェクトに動作を追加できます

関連記事:

IOの装飾パターン

デコレータパターンを使用する場合

3
Ravindra babu

@Federico A. Ramponiの優れた回答に基づいています。

この階層があると想像してください:

public interface IAnimal
{
    void DoSound();
}

public class Dog : IAnimal
{
    public void DoSound()
    {
        Console.WriteLine("Woof");
    }
}

public class Cat : IAnimal
{
    public void DoSound(IOperation o)
    {
        Console.WriteLine("Meaw");
    }
}

ここに「Walk」メソッドを追加する必要がある場合はどうなりますか?それは設計全体にとって苦痛です。

同時に、「ウォーク」メソッドを追加すると、新しい質問が生成されます。 「食べる」または「スリープ」はどうですか?追加する新しいアクションまたは操作ごとに、動物の階層に新しいメソッドを実際に追加する必要がありますか?それはくて最も重要なことで、Animalインターフェースを閉じることはできません。したがって、ビジターパターンを使用すると、階層を変更せずに新しいメソッドを階層に追加できます。

したがって、次のC#の例を確認して実行してください。

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var animals = new List<IAnimal>
            {
                new Cat(), new Cat(), new Dog(), new Cat(), 
                new Dog(), new Dog(), new Cat(), new Dog()
            };

            foreach (var animal in animals)
            {
                animal.DoOperation(new Walk());
                animal.DoOperation(new Sound());
            }

            Console.ReadLine();
        }
    }

    public interface IOperation
    {
        void PerformOperation(Dog dog);
        void PerformOperation(Cat cat);
    }

    public class Walk : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Dog walking");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Cat Walking");
        }
    }

    public class Sound : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Woof");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Meaw");
        }
    }

    public interface IAnimal
    {
        void DoOperation(IOperation o);
    }

    public class Dog : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }

    public class Cat : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }
}
3
Tomás Escamez

http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html の説明と例が本当に気に入っています。

前提は、固定のプライマリクラス階層があることです。おそらく別のベンダーのものであり、その階層を変更することはできません。ただし、その目的は、その階層に新しい多態性メソッドを追加することです。つまり、通常は基本クラスインターフェイスに何かを追加する必要があります。したがって、ジレンマは、基本クラスにメソッドを追加する必要があるが、基本クラスに触れることはできないということです。これをどうやって回避しますか?

この種の問題を解決する設計パターンは「訪問者」と呼ばれ(設計パターンブックの最後の1つ)、前のセクションで示した二重ディスパッチスキームに基づいています。

ビジターパターンを使用すると、Visitorタイプの別のクラス階層を作成してプライマリタイプで実行される操作を仮想化することにより、プライマリタイプのインターフェイスを拡張できます。プライマリタイプのオブジェクトは、単にビジターを「受け入れ」、ビジターの動的にバインドされたメンバー関数を呼び出します。

2
wojcikstefan

ncle bob article を見つけてコメントを読むまで、このパターンを理解していませんでした。次のコードを検討してください。

public class Employee
{
}

public class SalariedEmployee : Employee
{
}

public class HourlyEmployee : Employee
{
}

public class QtdHoursAndPayReport
{
    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        foreach (Employee e in employees)
        {
            if (e is HourlyEmployee he)
                PrintReportLine(he);
            if (e is SalariedEmployee se)
                PrintReportLine(se);
        }
    }

    public void PrintReportLine(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hours");
    }
    public void PrintReportLine(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    }
}

class Program
{
    static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }
}

Single Responsibility を確認しているため、見栄えは良いかもしれませんが、 Open/Closed の原則に違反しています。新しい従業員タイプを作成するたびに、タイプチェック付きの場合は追加する必要があります。そして、そうしないと、コンパイル時にそれを知ることは決してないでしょう。

ビジターパターンを使用すると、オープン/クローズの原則に違反せず、単一の責任に違反しないため、コードを簡潔にすることができます。そして、visitの実装を忘れるとコンパイルされません:

public abstract class Employee
{
    public abstract void Accept(EmployeeVisitor v);
}

public class SalariedEmployee : Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public class HourlyEmployee:Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public interface EmployeeVisitor
{
    void Visit(HourlyEmployee he);
    void Visit(SalariedEmployee se);
}

public class QtdHoursAndPayReport : EmployeeVisitor
{
    public void Visit(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hourly");
        // generate the line of the report.
    }
    public void Visit(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    } // do nothing

    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        QtdHoursAndPayReport v = new QtdHoursAndPayReport();
        foreach (var emp in employees)
        {
            emp.Accept(v);
        }
    }
}

class Program
{

    public static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }       
}  
}

魔法は、v.Visit(this)は同じように見えますが、訪問者のさまざまなオーバーロードを呼び出すため、実際は異なります。

1
Access Denied

いつ、どのように理解したのか、なぜ理解したことはありません。 C++のような言語のバックグラウンドを持つ人に役立つ場合は、 これを読んでください 非常に慎重に行う必要があります。

怠zyな場合は、ビジターパターンを使用します。 "C++で仮想関数が動的にディスパッチされる間、関数のオーバーロードは静的に行われる"

または、別の方法で、実際にApolloSpacecraftオブジェクトにバインドされているSpaceShip参照を渡すときにCollideWith(ApolloSpacecraft&)が呼び出されるようにします。

class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
  virtual void CollideWith(SpaceShip&) {
    cout << "ExplodingAsteroid hit a SpaceShip" << endl;
  }
  virtual void CollideWith(ApolloSpacecraft&) {
    cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
  }
}
1
Carl

ユニオンデータ型で関数オブジェクトを使用する場合は、ビジターパターンが必要になります。

関数オブジェクトとユニオンのデータ型が何であるか疑問に思うかもしれませんが、読む価値はあります http://www.ccs.neu.edu/home/matthias/htdc.html

0
Wei Qiu

@ Federico A. Ramponi の素晴らしい説明をありがとう、これをJavaバージョンで作成しました。役に立てば幸いです。

@ Konrad Rudolph が指摘したように、実際にはdouble dispatchを使用してtwo 実行時メソッドを決定するための具体的なインスタンスを一緒に。

したがって、実際には、operationexecutorに対してcommonインターフェースを作成する必要はありません。 operationインターフェイスが適切に定義されています。

import static Java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showTheHobby(food);
        Katherine katherine = new Katherine();
        katherine.presentHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void embed(Katherine katherine);
}


class Hearen {
    String name = "Hearen";
    void showTheHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine {
    String name = "Katherine";
    void presentHobby(Hobby hobby) {
        hobby.embed(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void embed(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

ご想像のとおり、commonインターフェースは、実際にはessential部分ではありませんが、より明確になりますこのパターン。

import static Java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showHobby(food);
        Katherine katherine = new Katherine();
        katherine.showHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void insert(Katherine katherine);
}

abstract class Person {
    String name;
    protected Person(String n) {
        this.name = n;
    }
    abstract void showHobby(Hobby hobby);
}

class Hearen extends  Person {
    public Hearen() {
        super("Hearen");
    }
    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine extends Person {
    public Katherine() {
        super("Katherine");
    }

    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void insert(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}
0
Hearen

あなたの質問はいつ知るべきかです:

私は最初に訪問者パターンでコードを書いていません。私は標準をコーディングし、発生するまで待ってからリファクタリングします。一度に1つずつインストールした複数の支払いシステムがあるとしましょう。チェックアウト時に、たとえば次のように多くのif条件(またはinstanceOf)を持つことができます。

//psuedo code
    if(Paypal) 
    do Paypal checkout 
    if(stripe)
    do strip stuff checkout
    if(payoneer)
    do payoneer checkout

今、私は10の支払い方法を持っていたと想像してください、それはちょっとugいです。そのため、そのようなパターンが発生した訪問者は、それらすべてを分離するために手近に来て、その後このようなものを呼び出すことになります:

new PaymentCheckoutVistor(paymentType).visit()

実装の方法は、ユースケースを示している多くの例から見ることができます。

0
j2emanue