ブログで訪問者パターンへの言及をよく見かけますが、認めざるを得ません。パターンのウィキペディアの記事を読み、その仕組みを理解しましたが、いつ使用するかについてはまだ混乱しています。
最近本当にデコレータ パターンを手に入れた人として、この一見便利なパターンを直感的に理解できるようになりたいと思っています。
ブログで訪問者パターンへの言及をよく見かけますが、認めざるを得ません。パターンのウィキペディアの記事を読み、その仕組みを理解しましたが、いつ使用するかについてはまだ混乱しています。
最近本当にデコレータ パターンを手に入れた人として、この一見便利なパターンを直感的に理解できるようになりたいと思っています。
私はビジターパターンにあまり慣れていません。私がそれを正しく理解したかどうか見てみましょう。動物の階層があるとします
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 パターンを使用すると、それぞれの新しい操作を適切なクラスに移動でき、階層のインターフェイスを一度だけ拡張する必要があります。やってみましょう。まず、階層内のすべてのクラスのメソッドを持つ抽象操作 ( 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);
}
ここにいる誰もが正しいですが、「いつ」に対処できていないと思います。まず、デザインパターンから:
Visitor を使用すると、操作対象の要素のクラスを変更せずに、新しい操作を定義できます。
ここで、単純なクラス階層について考えてみましょう。クラス 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 )
TreeNodevisit
アルゴリズムは、すべてのTreeNodeがVisitorのarrivedAt
メソッドへの引数として使用されることを保証します。
1 つの見方としては、ビジター パターンは、クライアントが特定のクラス階層内のすべてのクラスに追加のメソッドを追加できるようにする方法であるということです。
クラス階層がかなり安定しているが、その階層で何をする必要があるかという要件が変化している場合に役立ちます。
古典的な例は、コンパイラなどです。抽象構文木 (AST) はプログラミング言語の構造を正確に定義できますが、コード ジェネレーター、プリティ プリンター、デバッガー、複雑さのメトリック分析など、プロジェクトが進むにつれて AST で実行したい操作が変化します。
Visitor パターンがなければ、開発者が新しい機能を追加するたびに、基本クラスのすべての機能にそのメソッドを追加する必要がありました。これは、基本クラスが別のライブラリにある場合、または別のチームによって作成された場合に特に困難です。
(Visitor パターンは、データの操作をデータから遠ざけるため、OO の優れたプラクティスと矛盾すると主張していると聞いたことがあります。Visitor パターンは、通常の OO プラクティスが失敗する状況で正確に役立ちます。)
Visitor パターンを使用するのには、少なくとも 3 つの非常に適切な理由があります。
データ構造が変更されたときにわずかにしか変わらないコードの増殖を減らします。
計算を実装するコードを変更せずに、同じ計算をいくつかのデータ構造に適用します。
従来のコードを変更せずに、従来のライブラリに情報を追加します。
これについて私が書いた記事を見てください。
次のリンクで簡単に見つけました:
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
したがって、main メソッドでは、次のコードを使用して錠剤数を計算できます。
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
}
s の数をカウントする責任を、Pill
呼び出されたクラスに移動しましたPillCountVisitor
(また、switch case ステートメントを削除しました)。つまり、新しいタイプのピル コンテナーを追加する必要があるときはいつでも、PillCountVisitor
クラスのみを変更する必要があります。IVisitor
また、インターフェイスは別のシナリオで使用するための一般的なものであることに注意してください。
Accept メソッドをピル コンテナ クラスに追加すると、次のようになります。
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
方法は報告と代表のためのものであり、これは単一責任の原則に違反しています。したがって、この問題を解決するには、ビジター パターンを使用することをお勧めします。
Cay Horstmann は、彼の OO Design and patterns book で、Visitorを適用する場所の素晴らしい例を示しています。彼は問題を次のように要約しています。
複合オブジェクトは、多くの場合、個々の要素で構成される複雑な構造を持っています。一部の要素は、再び子要素を持つ場合があります。... 要素に対する操作は、その子要素にアクセスし、それらに操作を適用して、結果を結合します。…とはいえ、このような設計に新たな操作を追加するのは容易ではありません。
簡単ではない理由は、操作が構造体クラス自体に追加されるためです。たとえば、ファイル システムがあるとします。
この構造で実装したい操作 (機能) を次に示します。
FileSystem の各クラスに関数を追加して、操作を実装することができます (これは、方法が非常に明白であるため、過去に行われています)。問題は、新しい機能 (上記の「etc.」行) を追加するたびに、構造体クラスにますます多くのメソッドを追加する必要がある場合があることです。ある時点で、ソフトウェアにいくつかの操作を追加した後、これらのクラスのメソッドは、クラスの機能的結束という点で意味をなさなくなります。たとえば、ファイル システムに最新の視覚化機能を実装するためFileNode
のメソッドを持つ があります。calculateFileColorForFunctionABC()
ビジター パターンは (多くのデザイン パターンと同様に) 開発者の苦痛と苦しみから生まれました。開発者は、あらゆる場所で多くの変更を必要とせず、優れた設計原則 (高い結束、低い結合)。その痛みを感じるまで、多くのパターンの有用性を理解するのは難しいというのが私の意見です。痛みを説明すると (追加される「etc.」機能を使用して上で試みたように)、説明のスペースを取り、気を散らしてしまいます。このため、パターンを理解することは困難です。
Visitor を使用すると、データ構造 (例: FileSystemNodes
) の機能をデータ構造自体から切り離すことができます。このパターンにより、デザインはまとまりを尊重することができます。データ構造クラスはより単純になり (メソッドが少なくなります)、機能もVisitor
実装にカプセル化されます。これは、二重ディスパッチ(パターンの複雑な部分) によって行われます:accept()
構造体クラスのvisitX()
メソッドと Visitor (機能) クラスのメソッドを使用します。
この構造により、具体的なビジターとして構造上で機能する新しい機能を追加できます (構造クラスを変更する必要はありません)。
たとえばPrintNameVisitor
、ディレクトリ リスト機能を実装する やPrintSizeVisitor
、サイズ付きのバージョンを実装する などです。いつの日か、XML でデータを生成する「ExportXMLVisitor」や、JSON などでデータを生成する別のビジターがいると想像できます。視覚化するために、DOT などのグラフィカル言語を使用してディレクトリ ツリーを表示するビジターもいる可能性があります。別のプログラムで。
最後に、二重ディスパッチを伴う Visitor の複雑さは、理解、コーディング、およびデバッグがより困難であることを意味します。要するに、オタク要素が高く、KISS主義に逆行している。研究者が行った調査では、Visitor は物議を醸すパターンであることが示されました (その有用性についてはコンセンサスがありませんでした)。一部の実験では、コードの保守が容易にならないことが示されました。
私の意見では、新しい操作を追加するための作業量はVisitor Pattern
、各要素構造の使用または直接変更とほぼ同じです。また、たとえば新しい要素クラスを追加するとCow
、Operation インターフェースが影響を受け、これが既存のすべての要素クラスに伝播するため、すべての要素クラスの再コンパイルが必要になります。では、ポイントは何ですか?
アンクルボブの記事に出くわしてコメントを読むまで、私はこのパターンを理解していませんでした。次のコードを検討してください。
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();
}
}
単一の責任を確認しているため、見た目は良いかもしれませんが、オープン/クローズの原則に違反しています。新しい従業員タイプを作成するたびに、タイプ チェックを使用する場合は追加する必要があります。そうしないと、コンパイル時にそれを知ることはできません。
訪問者パターンを使用すると、オープン/クローズの原則に違反せず、単一の責任に違反しないため、コードをよりクリーンにすることができます。また、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");
}
}
ここに「Walk」メソッドを追加する必要がある場合はどうなりますか? それは設計全体にとって苦痛になります。
同時に、「Walk」メソッドを追加すると、新しい質問が生成されます。「食べる」か「寝る」か。追加したいすべての新しいアクションまたは操作に対して、アニマル階層に新しいメソッドを本当に追加する必要があるのでしょうか? これは醜く、最も重要なことです。アニマル インターフェイスを閉じることができなくなります。したがって、ビジター パターンを使用すると、階層を変更せずに新しいメソッドを階層に追加できます。
したがって、次の 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);
}
}
}
ビジターを使用すると、クラス自体を変更することなく、クラスのファミリーに新しい仮想関数を追加できます。代わりに、仮想関数の適切な特殊化をすべて実装するビジター クラスを作成します。
訪問者の構造:
次の場合は訪問者パターンを使用します。
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);
}
}
}
説明:
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
参照する
ソース作成記事
詳細については
パターンを使用すると、同じクラスの他のオブジェクトの動作に影響を与えることなく、静的または動的に個々のオブジェクトに動作を追加できます
関連記事:
方法と時期は理解できましたが、理由は理解できませんでした。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;
}
}