私はブログでビジターパターンへの言及を見続けていますが、認めざるを得ません。私はパターンについてウィキペディアの記事を読み、そのメカニズムを理解しましたが、それをいつ使用するかについてはまだ混乱しています。
最近本当にデコレータパターンを取得し、今やあらゆる場所で使用されている人として、この一見便利なパターンも直感的に理解できるようになりたいと思っています。
私はブログでビジターパターンへの言及を見続けていますが、認めざるを得ません。私はパターンについてウィキペディアの記事を読み、そのメカニズムを理解しましたが、それをいつ使用するかについてはまだ混乱しています。
最近本当にデコレータパターンを取得し、今やあらゆる場所で使用されている人として、この一見便利なパターンも直感的に理解できるようになりたいと思っています。
回答:
訪問者のパターンにはあまり詳しくありません。私がそれが正しいかどうか見てみましょう。動物の階層があるとします
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"; }
しかし、このようにして、操作を追加するたびに、階層のすべての単一クラスのインターフェースを変更する必要があります。ここで、代わりに元のインターフェースに満足しており、変更を最小限にしたいとします。
Visitorパターンを使用すると、新しい各操作を適切なクラスに移動でき、階層のインターフェースを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);
}
letsDo(Operation *v)
ポインタが必要です。
theSound.hereIsACat(c)
、仕事を終えたでしょうが、パターンによって生じるすべてのオーバーヘッドをどのように正当化しますか?二重発送は正当化です。
あなたの混乱の理由は、おそらくビジターが致命的な誤称であることです。多くの(著名な1!)プログラマーがこの問題に遭遇しました。それが実際に行うことは、それをネイティブにサポートしない言語でダブルディスパッチを実装することです(それらのほとんどはサポートしていません)。
1)私のお気に入りの例は、「Effective C ++」の著名な作家であるScott Meyersで、これを彼の最も重要なC ++の 1つと呼びました。これまでの瞬間。
switch
:switch
(クライアント側(コードの重複)で意思決定ハードコードおよび静的型チェックを提供していません。ケースの完全性や明瞭性などを確認してください)。ビジターパターンはタイプチェッカーによって検証され、通常はクライアントコードを単純化します。
virtual
が現代のプログラミング言語で機能が非常に便利な理由です-それらは拡張可能なプログラムの基本的なビルディングブロックです-私の意見では、Cの方法(選択した言語に応じてネストされたスイッチまたはパターンマッチなど)は拡張可能である必要のないコードがはるかにクリーンであり、証明者9のような複雑なソフトウェアでこのスタイルを目にして驚きました。さらに重要なことに、拡張性を提供したい言語は、再帰的な単一ディスパッチよりも優れたディスパッチパターンに対応する必要があります(つまり、ビジター)。
ここの全員が正しいですが、「いつ」に対処することはできません。まず、デザインパターンから:
訪問者を使用すると、操作する要素のクラスを変更せずに新しい操作を定義できます。
ここで、単純なクラス階層について考えてみましょう。クラス1、2、3、4とメソッドA、B、C、Dがあります。スプレッドシートのようにレイアウトします。クラスは行で、メソッドは列です。
現在、オブジェクト指向設計では、新しいメソッドよりも新しいクラスを成長させる可能性が高いと想定されているため、いわば行を追加する方が簡単です。新しいクラスを追加し、そのクラスの違いを指定し、残りを継承するだけです。
ただし、クラスが比較的静的な場合もありますが、メソッドを頻繁に追加する必要があります-列を追加します。OO設計の標準的な方法は、そのようなメソッドをすべてのクラスに追加することです。Visitorパターンはこれを簡単にします。
ちなみに、これはScalaのパターンマッチが解決しようとしている問題です。
ビジターデザインパターンは、ディレクトリツリー、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
メソッドは、構造内の各ノードにVisitorオブジェクトを適用します。この場合、それはトップダウンのビジターです。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
サブクラスは、各レベルでノードをカウントし、ノードのリストを蓄積して、適切なパス階層セクション番号を生成するなどの処理を実行できます。
これがアプリケーションです。ツリー構造を構築しますsomeTree
。それは作成されますVisitor
、dumpNodes
。
次にdumpNodes
、をツリーに適用します。dumpNode
オブジェクトは、ツリー内の各ノードを「訪問」します。
someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )
TreeNode visit
アルゴリズムは、すべてのTreeNodeが訪問者のarrivedAt
メソッドへの引数として使用されることを保証します。
これを見る1つの方法は、ビジターパターンは、クライアントが特定のクラス階層内のすべてのクラスにメソッドを追加できるようにする方法であるということです。
これは、かなり安定したクラス階層がある場合に役立ちますが、その階層で実行する必要がある要件が変化します。
古典的な例はコンパイラーなどです。抽象構文ツリー(AST)はプログラミング言語の構造を正確に定義できますが、ASTで実行する可能性のある操作は、プロジェクトの進行に伴って変化します(コードジェネレーター、プリティプリンター、デバッガー、複雑度メトリック分析)。
Visitorパターンがないと、開発者が新しい機能を追加するたびに、そのメソッドを基本クラスのすべての機能に追加する必要があります。これは、基本クラスが別のライブラリに表示される場合、または別のチームによって作成される場合は特に困難です。
(Visitorパターンは、データの操作をデータから遠ざけるため、適切なOOプラクティスと矛盾すると主張していると聞きました。Visitorパターンは、通常のOOプラクティスが失敗する状況で正確に役立ちます。)
ビジターパターンを使用する理由は少なくとも3つあります。
データ構造が変化してもわずかに異なるコードの急増を減らします。
計算を実装するコードを変更せずに、同じ計算を複数のデータ構造に適用します。
レガシーコードを変更せずに、レガシーライブラリに情報を追加します。
これについて私が書いた記事をご覧ください。
コンラッド・ルドルフがすでに指摘したように、二重派遣が必要な場合に適しています
ここでは、二重ディスパッチが必要な状況と、ビジターがどのように支援してくれるかを示す例を示します。
例:
たとえば、iPhone、Android、Windows Mobileの3種類のモバイルデバイスがあるとします。
これら3つのデバイスにはすべて、Bluetooth無線がインストールされています。
ブルートゥースラジオが2つの別個のOEM – Intel&Broadcomからのものであると仮定しましょう。
例を私たちの議論に関連させるために、Intelラジオによって公開されるAPIがBroadcomラジオによって公開されるものとは異なると仮定しましょう。
これが私のクラスの外観です–
ここで、モバイルデバイスのBluetoothをオンにする操作を紹介します。
関数のシグネチャは次のようになります–
void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)
依存するので、デバイスの右のタイプとBluetooth無線の右のタイプによっては、それがでオンにすることができ、適切な手順やアルゴリズムを呼び出します。
原則として、これは3 x 2のマトリックスになり、関係するオブジェクトの適切なタイプに応じて、適切な操作をベクトル化しようとしています。
両方の引数のタイプに応じた多態的な動作。
これで、Visitorパターンをこの問題に適用できます。Wikipediaのページからインスピレーションを得ています– 「基本的に、ビジターはクラス自体を変更せずに、クラスのファミリーに新しい仮想関数を追加できます。代わりに、仮想関数の適切な特殊化のすべてを実装するビジタークラスを作成します。訪問者はインスタンス参照を入力として受け取り、二重ディスパッチによって目標を実装します。」
3x2マトリックスのため、ここでは二重ディスパッチが必要です
私はリンクをたどる方が簡単だとわかりました:
で
http://www.remondo.net/visitor-pattern-example-csharp/私は例を示しVisitorパターンの利点は何であることを示しているが、モック例ことを発見しました。ここには、次の異なるコンテナクラスがあり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
useに気付く場合も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
}
Pill
sの数をカウントする責任を呼び出されたクラスに移動しました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つにすぎません。
ただし、単一のディスパッチパラダイムを使用する言語で二重以上のディスパッチを実装する単一の方法であることに注意してください。
パターンを使用する理由は次のとおりです。
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() {
...
}
}
そして、すべてのピースのサブクラスについて同じことです。
この設計を説明する図クラスは次のとおりです。
このアプローチには、3つの重要な欠点があります。
-などの行動performMove()
やcomputeIfKingCheck()
非常におそらく一般的なロジックを使用します。
例えば、どのような具体的にはPiece
、performMove()
最終的に特定の場所に現在のピースを設定し、潜在的に相手のピースを取るであろう。
関連する動作を収集するのではなく、複数のクラスに分割すると、何らかの方法で単一の責任パターンが無効になります。保守性を難しくします。
– サブクラスがcheckMoveValidity()
認識Piece
または変更する可能性のある処理であってはならない処理。
人間やコンピュータのアクションを超えたチェックです。このチェックは、プレーヤーが要求した各アクションで実行され、要求された駒の移動が有効であることを確認します。
そのため、Piece
インターフェースでそれを提供したくありません。
–ボット開発者にとって困難なチェスゲームでは、通常、アプリケーションは標準API(Piece
インターフェース、サブクラス、ボード、一般的な動作など)を提供し、開発者がボット戦略を充実させることができます。
それを可能にするには、データと動作がPiece
実装で密結合されていないモデルを提案する必要があります。
では、ビジターパターンを使いましょう!
2種類の構造があります。
–訪問を受け入れるモデルクラス(ピース)
–訪問者(移動操作)
以下は、パターンを示すクラス図です。
上部にはビジターがあり、下部にはモデルクラスがあります。
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
レシーバーに基づく呼び出しを提供します。
コンパイル時に、メソッドはaccept()
Pieceインターフェースのメソッドにバインドされ、実行時に、バインドされたメソッドがランタイムPiece
クラスで呼び出されます。
そしてaccept()
、2番目のディスパッチを実行するのはメソッド実装です。
実際、オブジェクト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
し、これらのようなそのサブクラスがある私たちのクラス。
組み込みまたはサードパーティのクラスでは、物事はそれほど簡単ではありません。メソッド
を追加するには、それらをラップまたは継承する(可能な場合)必要がありaccept()
ます。
3)インダイレクション
パターンは複数の間接参照を作成します。
二重ディスパッチとは、単一の呼び出しではなく、2つの呼び出しを意味します。
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
そして、訪問者が訪問したオブジェクトの状態を変更するときに、追加の間接参照を持つことができます。
サイクルのように見えるかもしれません:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)
Cay Horstmannは、OOデザインとパターンの本でVisitorをどこに適用するかを示す良い例を持っています。彼は問題を要約します:
複合オブジェクトは、多くの場合、個々の要素で構成される複雑な構造を持っています。一部の要素には、子要素が含まれる場合があります。...要素に対する操作は、その子要素を訪問し、それらに子操作を適用して、結果を結合します。...しかし、そのような設計に新しい操作を追加することは容易ではありません。
簡単ではない理由は、構造クラス自体に操作が追加されるためです。たとえば、ファイルシステムがあるとします。
この構造で実装する可能性のあるいくつかの操作(機能)を次に示します。
FileSystemの各クラスに関数を追加して、操作を実装することができます(方法は非常に明白であるため、以前はこれを行っていました)。問題は、新しい機能(上記の「etc.」行)を追加するたびに、構造クラスにメソッドを追加しなければならない可能性があることです。ある時点で、ソフトウェアにいくつかの操作を追加した後、それらのクラスのメソッドは、クラスの機能的結合の観点からもはや意味をなさなくなります。たとえば、ファイルシステムに最新の視覚化機能を実装するためのFileNode
メソッドを持つがあるcalculateFileColorForFunctionABC()
とします。
ビジターパターン(多くのデザインパターンと同様)は、多くの変更を必要とせずにコードを変更できるより良い方法があり、優れたデザイン原則(高い凝集性、低い結合)を尊重する開発者の苦痛と苦痛から生まれました。)。あなたがその痛みを感じるまで、多くのパターンの有用性を理解するのは難しいと私は思います。痛みを説明することは(追加された「etc.」機能を使用して上記のようにしようとすることですが)、説明のスペースを占め、注意散漫になります。このため、パターンを理解することは困難です。
ビジターを使用すると、データ構造(たとえばFileSystemNodes
)の機能をデータ構造自体から切り離すことができます。このパターンにより、設計はまとまりを尊重することができます。データ構造クラスはより単純で(メソッドの数は少ない)、機能はVisitor
実装にカプセル化されます。これは、二重ディスパッチ(パターンの複雑な部分)を介して行われaccept()
ます。構造クラスのvisitX()
メソッドとビジター(機能)クラスのメソッドを使用します。
この構造により、(構造クラスを変更せずに)具象ビジターとして構造に作用する新しい機能を追加できます。
たとえばPrintNameVisitor
、ディレクトリリスト機能PrintSizeVisitor
を実装すると、サイズ付きのバージョンを実装するです。XMLでデータを生成する「ExportXMLVisitor」、またはJSONなどでデータを生成する別のビジターがいると想像できます。DOTなどのグラフィカル言語を使用してディレクトリツリーを表示するビジターを視覚化することもできます別のプログラムで。
最後に、ダブルディスパッチによるビジターの複雑さは、理解、コーディング、デバッグが困難であることを意味します。要するに、それは高いオタク要素を持ち、KISS原理に逆行します。研究者が行った調査では、Visitorは物議を醸しているパターンであることが示されていました(その有用性についてコンセンサスはありませんでした)。いくつかの実験は、それがコードを維持することをより簡単にしないことさえ示しました。
私の意見では、新しい操作を追加するための作業量はVisitor Pattern
、各要素の構造を使用するか直接変更することで、ほぼ同じです。また、たとえばCow
、新しい要素クラスを追加すると、Operationインターフェースが影響を受け、これがすべての既存の要素クラスに伝播するため、すべての要素クラスの再コンパイルが必要になります。だからポイントは何ですか?
rootElement.visit (node) -> node.collapse()
。ビジターを使用すると、各ノードはそのすべての子に対してグラフ走査を実装するので、完了です。
levelsRemaining
パラメータとしてカウンタを簡単に渡す。次のレベルの子供を呼び出す前にそれを減らします。あなたの訪問者の内部if(levelsRemaining == 0) return
。
ビジターパターンの簡単な説明。変更が必要なクラスはすべて「accept」メソッドを実装する必要があります。クライアントは、このacceptメソッドを呼び出して、そのクラスのファミリーでいくつかの新しいアクションを実行し、それによって機能を拡張します。クライアントは、この1つのacceptメソッドを使用して、特定のアクションごとに異なるビジタークラスを渡すことにより、幅広い新しいアクションを実行できます。訪問者クラスには、ファミリ内のすべてのクラスに対して同じ特定のアクションを達成する方法を定義する、複数のオーバーライドされた訪問メソッドが含まれています。これらのvisitメソッドには、動作するインスタンスが渡されます。
使用を検討する可能性がある場合
ボブおじさんの記事を見つけてコメントを読むまでは、このパターンを理解していませんでした。次のコードを検討してください。
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();
}
}
単一の責任であることが確認されているため、見た目は良いかもしれませんが、オープン/クローズの原則に違反しています。型チェックを行う場合は、新しいEmployeeタイプを作成するたびに追加する必要があります。そして、あなたが知らないなら、コンパイル時にそれを決して知ることはないでしょう。
ビジターパターンを使用すると、オープン/クローズの原則に違反せず、単一の責任に違反しないため、コードをよりクリーンにすることができます。また、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)
同じように見えますが、ビジターの異なるオーバーロードを呼び出すため、実際には異なります。
@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");
}
}
ここに「ウォーク」メソッドを追加する必要がある場合はどうなりますか?それはデザイン全体にとって苦痛です。
同時に、「ウォーク」メソッドを追加すると、新しい質問が生成されます。「食べる」や「寝る」はどうですか?追加するすべての新しいアクションまたは操作に対して、Animal階層に新しいメソッドを本当に追加する必要がありますか?これは醜く、最も重要なことです。どうすれば、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);
}
}
}
Dog
と同様にCat
。それらを継承するか、適切な例を選択するために、それらを基本クラスで作成することもできます。
訪問者は、クラス自体を変更することなく、クラスのファミリーに新しい仮想関数を追加できます。代わりに、仮想関数の適切な特殊化のすべてを実装するビジタークラスを作成します
訪問者の構造:
次の場合は、訪問者パターンを使用します。
にもかかわらずビジターパターンは、オブジェクトの既存のコードを変更せずに新しい操作を追加するための柔軟性を提供し、この柔軟性は欠点が付属しています。
新しい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);
}
}
}
説明:
Visitable
(Element
)はインターフェースであり、このインターフェースメソッドをクラスのセットに追加する必要があります。Visitor
Visitable
要素の操作を実行するメソッドを含むインターフェースです。GameVisitor
Visitor
インターフェース(ConcreteVisitor
)を実装するクラスです。Visitable
要素はVisitor
、関連するVisitor
インターフェースのメソッドを受け入れて呼び出します。Game
ようElement
ようなゲームやコンクリート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
参照する
ソースメイキング記事
詳細については
パターンを使用すると、同じクラスの他のオブジェクトの動作に影響を与えることなく、静的または動的に個々のオブジェクトに動作を追加できます
関連記事:
http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.htmlの説明と例が本当に気に入っています。
前提は、固定されたプライマリクラス階層があることです。おそらくそれは別のベンダーからのものであり、その階層を変更することはできません。ただし、その意図は、その階層に新しいポリモーフィックメソッドを追加することです。つまり、通常は基本クラスインターフェイスに何かを追加する必要があります。したがって、ジレンマは、基本クラスにメソッドを追加する必要があるということですが、基本クラスに触れることはできません。これをどうやって回避しますか?
この種の問題を解決する設計パターンは「ビジター」(「設計パターン」ブックの最後の1つ)と呼ばれ、前のセクションで示した二重ディスパッチスキームに基づいています。
訪問者パターンを使用すると、タイプVisitorの個別のクラス階層を作成して、主タイプで実行される操作を仮想化することにより、主タイプのインターフェースを拡張できます。プライマリタイプのオブジェクトは、ビジターを単に「受け入れ」、ビジターの動的にバインドされたメンバー関数を呼び出します。
いつ、どのようにして理解したのか、その理由を理解したことがありません。C ++のような言語の背景を持つ人を助ける場合は、これを非常に注意深く読んでください。
「仮想関数が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;
}
}
@Federico A. Ramponiの素晴らしい説明をありがとう、私はちょうどこれをJavaバージョンで作成しました。お役に立てれば幸いです。
また、同じように@Konradルドルフが指摘し、それが実際だダブルディスパッチ使用して2つのランタイムメソッドを決定するために一緒に、具体的な事例を。
そのため、実際に操作インターフェイスが適切に定義されている限り、操作エグゼキュータに共通のインターフェイスを作成する必要はありません。
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");
}
}
ご想像のとおり、このパターンでは本質的に重要な部分ではありませんが、共通のインターフェイスを使用するとわかりやすくなります。
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");
}
}
あなたの質問はいつ知るかです:
最初にビジターパターンでコードを記述しません。私は標準をコーディングし、必要が生じるのを待ってからリファクタリングします。したがって、一度に1つずつインストールした複数の支払いシステムがあるとします。チェックアウト時に、たとえば次のような多くのif条件(またはinstanceOf)が存在する可能性があります。
//psuedo code
if(payPal)
do paypal checkout
if(stripe)
do strip stuff checkout
if(payoneer)
do payoneer checkout
今、私が10種類の支払い方法を持っていると想像してみてください。そのため、そのようなパターンが発生するのを見ると、ビジターがそのすべてを分離するのに役立ち、その後、次のようなものを呼び出すことになります。
new PaymentCheckoutVistor(paymentType).visit()
実装例は、ユースケースを示すだけで、ここにある多くの例から実装方法を確認できます。