私はいつも、オブジェクトがそれに作用するためにデータとメッセージが必要だと思っていました。オブジェクトに外部的なメソッドが必要なのはいつですか。訪問者を迎えるには、どのような経験則に従いますか?これは、オブジェクトグラフを完全に制御できることを前提としています。
9 に答える
ビジターパターンは、トラバーサルが重要な非常に複雑なデータ構造のすべての要素に操作を適用する場合(たとえば、要素を並列にトラバースする場合、または高度に相互接続されたデータ構造をトラバースする場合)、またはダブルディスパッチを実装する場合に特に役立ちます。要素を順番に処理する必要があり、ダブルディスパッチが不要な場合は、カスタムIterable and Iteratorを実装することをお勧めします。特に、他のAPIとの適合性が高いためです。
私はいつも、オブジェクトがそれに作用するためにデータとメッセージが必要だと思っていました。オブジェクトに外部的なメソッドが必要なのはいつですか。訪問者を迎えるには、どのような経験則に従いますか?これは、オブジェクトグラフを完全に制御できることを前提としています。
特定のオブジェクトのすべての動作を1つのクラスで定義すると便利でない場合があります。たとえば、Javaでは、元々別のモジュールで定義された一連のクラスにメソッドを実装する必要がある場合、元のクラスファイル以外の場所にtoXml
書き込むことができないため、複雑になります。つまり、変更せずにシステムを拡張することはできません。既存のソース(Smalltalkまたは他の言語では、特定のファイルに関連付けられていないメソッドを拡張子でグループ化できます)。toXml
より一般的には、静的に型付けされた言語には、(1)既存のデータ型に新しい関数を追加する機能と(2)同じ関数をサポートする新しいデータ型の実装を追加する機能の間に緊張関係があります-これは式の問題と呼ばれます(ウィキペディアページ)。
オブジェクト指向言語はポイント2で優れています。インターフェースがあれば、新しい実装を安全かつ簡単に追加できます。機能言語はポイント1で優れています。パターンマッチング/アドホックポリモーフィズム/オーバーロードに依存しているため、既存のタイプに新しい関数を簡単に追加できます。
ビジターパターンは、オブジェクト指向設計でポイント1をサポートする方法です。タイプセーフな方法で新しい動作を使用してシステムを簡単に拡張できます(手動でパターンマッチングを行う場合はそうではありませんif-else-instanceof
。ケースがカバーされていない場合、言語は決して警告しません)。
訪問者は通常、既知のタイプの固定セットがある場合に使用されます。これは、「オブジェクトグラフの完全な制御」が意味するものだと思います。例としては、パーサー内のトークン、さまざまなタイプのノードを持つツリー、および同様の状況があります。
結論として、私はあなたがあなたの分析に正しかったと言うでしょう:)
PS:ビジターパターンは複合パターンでうまく機能しますが、個別にも役立ちます
時にはそれは組織の問題です。n種類のオブジェクト(例:クラス)とm種類の操作(例:メソッド)がある場合、n * mのクラス/メソッドのペアをクラスまたはメソッドごとにグループ化する必要がありますか?ほとんどのオブジェクト指向言語は、物事をクラスごとにグループ化することに強く傾いていますが、操作ごとに整理する方が理にかなっている場合もあります。たとえば、コンパイラのようにオブジェクトグラフのマルチフェーズ処理では、特定の種類で発生する可能性のあるすべての操作を考えるよりも、各フェーズ(つまり操作)を1つの単位として考える方が便利なことがよくあります。ノードの。
厳密に組織化されているだけではないVisitorパターンの一般的なユースケースは、不要な依存関係を解消することです。たとえば、「データ」オブジェクトをプレゼンテーション層に依存させることは、特に複数のプレゼンテーション層があると想像する場合は、一般的に望ましくありません。ビジターパターンを使用することにより、プレゼンテーション層の詳細は、データオブジェクトのメソッドではなく、ビジターオブジェクトに存在します。データオブジェクト自体は、抽象的な訪問者インターフェイスについてのみ知っています。
ステートフルになるメソッドをEntity/DataObject / BusinessObjectに配置したいが、そのステートフル性をオブジェクトに導入したくない場合によく使用します。ステートフルビジターは、ジョブを実行したり、ステートフルでないデータオブジェクトからステートフルエグゼキューターオブジェクトのコレクションを生成したりできます。作業の処理がエグゼキュータスレッドにファームアウトされる場合に特に役立ちます。多くのステートフルビジター/ワーカーは、ステートフルでないオブジェクトの同じグループを参照できます。
私にとって、ビジターパターンを使用する唯一の理由は、ツリー/トライのようなグラフのようなデータ構造に対してダブルディスパッチを実行する必要がある場合です。
次の問題がある場合:
異種の集合構造のノードオブジェクトに対して、多くの別個の無関係な操作を実行する必要があります。これらの操作でノードクラスを「汚染」することは避けたいと考えています。また、目的の操作を実行する前に、各ノードのタイプを照会して、ポインターを正しいタイプにキャストする必要はありません。
次に、次のいずれかの目的でビジターパターンを使用できます。
- オブジェクト構造の要素に対して実行される操作を表します。
- 操作する要素のクラスを変更せずに、新しい操作を定義します。
- 失われたタイプ情報を回復するための古典的な手法。
- 2つのオブジェクトのタイプに基づいて正しいことを行います。
- ダブルディスパッチ
ビジターパターンは、(クラス階層内の)オブジェクトタイプによって動作を変更する必要がある場合に最も役立ちます。その動作は、オブジェクトによって提供されるパブリックインターフェイスの観点から定義できます。動作はそのオブジェクトに固有のものではなく、オブジェクトによるカプセル化の恩恵を受けたり、カプセル化する必要はありません。
訪問者は、各ノードがクラス階層の一部であるオブジェクトのグラフ/ツリーで自然に発生することがよくあります。クライアントがグラフ/ツリーを歩き、各タイプのノードを均一に処理できるようにするために、Visitorパターンは実際には最も単純な代替手段です。
たとえば、XML DOMについて考えてみます。ノードは基本クラスであり、要素、属性、およびその他のタイプのノードがクラス階層を定義します。
要件がDOMをJSONとして出力することであると想像してください。動作はノードに固有のものではありません-もしそうなら、クライアントが必要とする可能性のあるすべてのフォーマット(、など)を処理するためにノードにメソッドを追加する必要toJSON()
がtoASN1()
ありtoFastInfoSet()
ますtoXML()
。ほとんどのクライアントで使用される予定であり、概念的にはDOMに「近い」ため、便宜上提供されます。したがって、toXMLは、利便性のためにNodeに固有にすることができます。ただし、必須ではなく、次のように処理できます。他のすべての形式。
Nodeとそのサブクラスは、それらの状態をメソッドとして完全に利用できるようにするため、DOMを何らかの出力形式に変換できるようにするために外部で必要なすべての情報があります。次に、出力メソッドをNodeオブジェクトに配置する代わりに、Nodeに抽象accept()
メソッドを使用し、各サブクラスに実装する、Visitorインターフェイスを使用できます。
各ビジターメソッドの実装は、各ノードタイプのフォーマットを処理します。必要なすべての状態が各ノードタイプのメソッドから利用できるため、これを行うことができます。
ビジターを使用することで、各ノードクラスにその機能を負担させることなく、必要な出力形式を実装するための扉が開かれます。
インターフェイスを実装するクラスについて十分に理解している場合は、訪問者を使用することを常にお勧めします。このようにして、それほどきれいではない呼び出しを行うことはなくinstanceof
、コードははるかに読みやすくなります。また、訪問者が実装されると、現在および将来の多くの場所で再利用できます。
ビジターパターンは、ダブルディスパッチの問題に対する非常に自然な解決策です。ダブルディスパッチ問題は動的ディスパッチ問題のサブセットであり、実行時に決定される仮想(オーバーライド)メソッドとは異なり、メソッドのオーバーロードはコンパイル時に静的に決定されるという事実に起因します。
このシナリオを考えてみましょう。
public class CarOperations {
void doCollision(Car car){}
void doCollision(Bmw car){}
}
public class Car {
public void doVroom(){}
}
public class Bmw extends Car {
public void doVroom(){}
}
public static void Main() {
Car bmw = new Bmw();
bmw.doVroom(); //calls Bmw.doVroom() - single dispatch, works out that car is actually Bmw at runtime.
CarOperations carops = new CarOperations();
carops.doCollision(bmw); //calls CarOperations.doCollision(Car car) because compiler chose doCollision overload based on the declared type of bmw variable
}
以下のこのコードは、私の以前の回答から採用され、 Javaに変換されています。問題は上記のサンプルとは多少異なりますが、Visitorパターンの本質を示しています。
//This is the car operations interface. It knows about all the different kinds of cars it supports
//and is statically typed to accept only certain ICar subclasses as parameters
public interface CarVisitor {
void StickAccelerator(Toyota car);
void ChargeCreditCardEveryTimeCigaretteLighterIsUsed(Bmw car);
}
//Car interface, a car specific operation is invoked by calling PerformOperation
public interface Car {
public string getMake();
public void setMake(string make);
public void performOperation(CarVisitor visitor);
}
public class Toyota implements Car {
private string make;
public string getMake() {return this.make;}
public void setMake(string make) {this.make = make;}
public void performOperation(CarVisitor visitor) {
visitor.StickAccelerator(this);
}
}
public class Bmw implements Car{
private string make;
public string getMake() {return this.make;}
public void setMake(string make) {this.make = make;}
public void performOperation(ICarVisitor visitor) {
visitor.ChargeCreditCardEveryTimeCigaretteLighterIsUsed(this);
}
}
public class Program {
public static void Main() {
Car car = carDealer.getCarByPlateNumber("4SHIZL");
CarVisitor visitor = new SomeCarVisitor();
car.performOperation(visitor);
}
}