Yan Hon 博士の著書『JAVA and Patterns』は、Visitor パターンの説明で始まります。
訪問者パターンはオブジェクトの行動パターンです。ビジター パターンの目的は、特定のデータ構造要素に適用されるいくつかの操作をカプセル化することです。これらの操作を変更する必要がある場合、この操作を受け入れるデータ構造は変更しないで済みます。
派遣の概念
変数が宣言されたときの型は、変数の静的型 (Static Type) と呼ばれます。また、静的型を見かけの型 (Appparent Type) と呼ぶ人もいます。また、変数によって参照されるオブジェクトの実際の型も、変数の実際の型 (実際の型)。例えば:
次のようにコードをコピーします。
リスト list = null;
リスト = 新しい ArrayList();
変数リストが宣言され、その静的型 (自明の型とも呼ばれます) は List で、その実際の型は ArrayList です。
オブジェクトの種類に応じたメソッドの選択は、ディスパッチには静的ディスパッチと動的ディスパッチの 2 種類があります。
静的ディスパッチはコンパイル時に発生し、静的型情報に基づいてディスパッチが行われます。静的ディスパッチは私たちにとってメソッドのオーバーロードには馴染みのないものです。
動的ディスパッチは実行時に発生し、メソッドを動的に置き換えます。
静的ディスパッチ
Java は、メソッドのオーバーロードによる静的ディスパッチをサポートしています。墨子が馬に乗っている話を例として挙げると、墨子は白い馬にも黒い馬にも乗ることができます。墨子と白馬、黒馬、馬のクラス図は以下の通りです。
このシステムでは、Mozi は Mozi クラスによって表されます。コードは次のとおりです。
パブリッククラス Mozi {
パブリック ボイド ライド(馬 h){
System.out.println("乗馬");
}
パブリック ボイド ライド(WhiteHorse wh){
System.out.println("白い馬に乗って");
}
パブリック ボイド ライド(BlackHorse bh){
System.out.println("ダークホースに乗ろう");
}
public static void main(String[] args) {
Horse wh = new WhiteHorse();
Horse bh = new BlackHorse();
モジモジ = new Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
明らかに、Mozi クラスのride() メソッドは 3 つのメソッドからオーバーロードされています。これら 3 つのメソッドは、それぞれ Horse、WhiteHorse、BlackHorse、およびその他のタイプのパラメーターを受け入れます。
それでは、プログラムを実行するとどのような結果が出力されるのでしょうか?その結果、プログラムは同じ 2 行の「horseback」を出力します。言い換えれば、墨子は自分が乗っているのは馬だけであることに気づきました。
なぜ? ride() メソッドへの 2 つの呼び出しでは、異なるパラメータ、つまり wh と bh が渡されます。実際のタイプは異なりますが、静的なタイプはすべて同じであり、Horse タイプです。
オーバーロードされたメソッドのディスパッチは静的型に基づいており、このディスパッチ プロセスはコンパイル時に完了します。
動的ディスパッチ
Java は、メソッドのオーバーライドによる動的ディスパッチをサポートしています。草を食べる馬の話を例として使用すると、コードは次のようになります。
次のようにコードをコピーします。
パブリッククラスの馬 {
public void Eat(){
System.out.println("草を食べる馬");
}
}
次のようにコードをコピーします。
パブリック クラス BlackHorse extends Horse {
@オーバーライド
public void Eat() {
System.out.println("草を食べるダークホース");
}
}
次のようにコードをコピーします。
パブリック クラス クライアント {
public static void main(String[] args) {
Horse h = new BlackHorse();
熱();
}
}
変数 h の静的型は Horse で、実際の型は BlackHorse です。上記の最後の行の Eat() メソッドが BlackHorse クラスの Eat() メソッドを呼び出した場合、上記の Eat() メソッドが Eat( Horse クラスの) メソッドを実行すると、「馬は草を食べる」と出力されます。
したがって、問題の核心は、Java コンパイラはオブジェクトの静的型のみを認識し、オブジェクトの実際の型とメソッドを認識しないため、コンパイル中にどのコードが実行されるかを常に認識できるわけではないことです。呼び出しは、静的型ではなく、オブジェクトの実数型に基づいています。このように、上の最後の行の Eat() メソッドは、BlackHorse クラスの Eat() メソッドを呼び出し、「草を食べる黒い馬」を出力します。
派遣の種類
メソッドが属するオブジェクトをメソッドのレシーバと呼び、メソッドのパラメータを総称してメソッドのボリュームと呼びます。たとえば、以下の例の Test クラスのコピー コードは次のとおりです。
パブリック クラス テスト {
public void print(String str){
System.out.println(str);
}
}
上記のクラスでは、print() メソッドは Test オブジェクトに属しているため、そのレシーバーも Test オブジェクトになります。 print() メソッドには str というパラメータがあり、その型は String です。
何種類のディスパッチに基づくことができるかに応じて、オブジェクト指向言語は単一ディスパッチ言語 (Uni-Dispatch) と複数ディスパッチ言語 (Multi-Dispatch) に分類できます。シングルディスパッチ言語は 1 つのインスタンスのタイプに基づいてメソッドを選択しますが、マルチディスパッチ言語は複数のインスタンスのタイプに基づいてメソッドを選択します。
C++ と Java はどちらもシングルディスパッチ言語であり、マルチディスパッチ言語の例には CLOS や Cecil などがあります。この区別によれば、Java は、この言語の動的ディスパッチではメソッド レシーバーの型のみが考慮されるため、動的シングル ディスパッチ言語であり、この言語はオーバーロードされたメソッドをディスパッチするため、静的マルチディスパッチ言語です。メソッドのレシーバーのタイプと、メソッドのすべてのパラメータのタイプが考慮されます。
動的単一ディスパッチをサポートする言語では、リクエストがどのオペレーションを呼び出すかを決定する条件が 2 つあります。1 つはリクエストの名前で、もう 1 つはレシーバーの実際のタイプです。単一ディスパッチでは、メソッド選択プロセスが制限されるため、通常はメソッドの受信者である 1 つのインスタンスのみが考慮されます。 Java 言語では、不明な型のオブジェクトに対して操作が実行される場合、オブジェクトの実型テストは 1 回だけ行われます。これが動的シングル ディスパッチの特徴です。
二重発送
メソッドは 2 つの変数の型に基づいて異なるコードを実行することを決定します。これが「ダブルディスパッチ」です。 Java 言語は動的複数ディスパッチをサポートしていません。つまり、Java は動的二重ディスパッチをサポートしていません。ただし、デザイン パターンを使用すると、動的な二重ディスパッチを Java 言語で実装することもできます。
Java では、2 つのメソッド呼び出しを通じて 2 つのディスパッチを実現できます。クラス図は次のとおりです。
写真には 2 つのオブジェクトがあり、左側のオブジェクトは West と呼ばれ、右側のオブジェクトは East と呼ばれます。ここで、West オブジェクトは最初に East オブジェクトの goEast() メソッドを呼び出し、自分自身を渡します。 East オブジェクトが呼び出されると、渡されたパラメータに基づいて呼び出し元が誰であるかがすぐにわかるため、「呼び出し元」オブジェクトの goWest() メソッドが順番に呼び出されます。 2 つの呼び出しを通じて、プログラム制御が 2 つのオブジェクトに順番に渡されます。シーケンス図は次のとおりです。
このように、プログラム コントロールは 2 つのオブジェクト間で渡され、最初に West オブジェクトから East オブジェクトに渡され、次に West オブジェクトに戻されます。
しかし、ボールを返すだけでは二重分配の問題は解決しません。重要なのは、これら 2 つの呼び出しと Java 言語の動的単一ディスパッチ関数を使用して、この受け渡しプロセス中に 2 つの単一ディスパッチをトリガーする方法です。
Java 言語における動的単一ディスパッチは、サブクラスが親クラスのメソッドをオーバーライドするときに発生します。つまり、以下に示すように、West と East の両方を独自の型階層に配置する必要があります。
ソースコード
West クラスのコピーコードは次のとおりです。
パブリック抽象クラス West {
public abstract void goWest1(SubEast1 east);
public abstract void goWest2(SubEast2 east);
}
SubWest1 クラスのコピー コードは次のとおりです。
public class SubWest1 extends West{
@オーバーライド
public void goWest1(SubEast1 east) {
System.out.println("SubWest1 + " + east.myName1());
}
@オーバーライド
public void goWest2(SubEast2 east) {
System.out.println("SubWest1 + " + east.myName2());
}
}
サブウェストクラス2
次のようにコードをコピーします。
public class SubWest2 extends West{
@オーバーライド
public void goWest1(SubEast1 east) {
System.out.println("SubWest2 + " + east.myName1());
}
@オーバーライド
public void goWest2(SubEast2 east) {
System.out.println("SubWest2 + " + east.myName2());
}
}
East クラスのコピーコードは次のとおりです。
パブリック抽象クラス East {
public abstract void goEast(Westwest);
}
SubEast1 クラスのコピー コードは次のとおりです。
パブリック クラス SubEast1 extend East{
@オーバーライド
public void goEast(Westwest) {
west.goWest1(これ);
}
public String myName1(){
「SubEast1」を返します。
}
}
SubEast2クラスのコピーコードコードは次のとおりです。
パブリック クラス SubEast2 extend East{
@オーバーライド
public void goEast(Westwest) {
west.goWest2(これ);
}
public String myName2(){
「SubEast2」を返します。
}
}
クライアント クラスのコピー コードは次のとおりです。
パブリック クラス クライアント {
public static void main(String[] args) {
//組み合わせ1
East east = new SubEast1();
西西 = new SubWest1();
east.go東(西);
//組み合わせ2
east = 新しい SubEast1();
西 = 新しい SubWest2();
east.go東(西);
}
}
実行結果は次のとおりです。 コードは次のとおりです。
サブウェスト 1 + サブイースト 1
サブウェスト 2 + サブイースト 1
システムの実行中に、まず SubWest1 オブジェクトと SubEast1 オブジェクトが作成され、次にクライアントが SubEast1 の goEast() メソッドを呼び出して、SubWest1 オブジェクトを渡します。 SubEast1 オブジェクトはそのスーパークラス East の goEast() メソッドをオーバーライドするため、この時点で動的単一ディスパッチが発生します。 SubEast1 オブジェクトは呼び出しを受け取ると、パラメータから SubWest1 オブジェクトを取得するため、すぐにこのオブジェクトの goWest1() メソッドを呼び出し、自分自身を渡します。 SubEast1 オブジェクトにはどのオブジェクトを呼び出すかを選択する権利があるため、この時点で別の動的メソッドのディスパッチが実行されます。
この時点で、SubWest1 オブジェクトは SubEast1 オブジェクトを取得しています。このオブジェクトの myName1() メソッドを呼び出すと、自分の名前と SubEast オブジェクトの名前を出力できます。シーケンス図は次のとおりです。
これら 2 つの名前の一方は East 階層に由来し、もう一方は West 階層に由来するため、その組み合わせは動的に決定されます。これが動的二重ディスパッチの実装メカニズムです。
訪問者パターンの構造
ビジター パターンは、比較的未決定のデータ構造を持つシステムに適しています。これにより、データ構造とその構造に作用する操作の間の結合が分離され、一連の操作を比較的自由に進化させることができます。訪問者のパターンを簡略化した図を以下に示します。
データ構造の各ノードはビジターからの呼び出しを受け入れることができ、このノードはノード オブジェクトをビジター オブジェクトに渡し、ビジター オブジェクトはノード オブジェクトの操作を実行します。このプロセスは「二重ディスパッチ」と呼ばれます。ノードはビジターを呼び出して自分自身を渡し、ビジターはこのノードに対してアルゴリズムを実行します。 Visitor パターンの概略クラス図を以下に示します。
訪問者モードに関係する役割は次のとおりです。
●抽象訪問者 (Visitor) ロール: すべての特定の訪問者ロールが実装する必要があるインターフェイスを形成する 1 つ以上のメソッド操作を宣言します。
●具体的な訪問者 (ConcreteVisitor) ロール: 抽象訪問者によって宣言されたインターフェイス、つまり抽象訪問者によって宣言された各アクセス操作を実装します。
●抽象ノード (Node) ロール: 受け入れ操作を宣言し、訪問者オブジェクトをパラメータとして受け入れます。
● ConcreteNode ロール: 抽象ノードによって指定された受け入れ操作を実装します。
●構造オブジェクト (ObjectStructure) の役割: 次の役割を持ち、構造内のすべての要素を横断できます。必要に応じて、訪問者オブジェクトが各要素にアクセスできるように高レベルのインターフェイスを提供し、複合オブジェクトとして設計することもできます。 List や Set などのコレクション。
ソースコード
ご覧のとおり、抽象訪問者ロールは、特定のノードごとにアクセス操作を準備します。ノードが 2 つあるため、対応するアクセス操作が 2 つあります。
次のようにコードをコピーします。
パブリック インターフェイス 訪問者 {
/**
※NodeAのアクセス動作に相当
*/
public void visit(NodeA ノード);
/**
※NodeBのアクセス動作に対応
*/
public void visit(NodeB ノード);
}
特定の VisitorA クラスのコピー コードは次のとおりです。
public class VisitorA は Visitor { を実装します
/**
※NodeAのアクセス動作に相当
*/
@オーバーライド
public void visit(NodeA ノード) {
System.out.println(node.operationA());
}
/**
※NodeBのアクセス動作に対応
*/
@オーバーライド
public void visit(NodeB ノード) {
System.out.println(node.operationB());
}
}
特定の訪問者の VisitorB クラスのコピー コードは次のとおりです。
public class VisitorB は Visitor { を実装します
/**
※NodeAのアクセス動作に相当
*/
@オーバーライド
public void visit(NodeA ノード) {
System.out.println(node.operationA());
}
/**
※NodeBのアクセス動作に対応
*/
@オーバーライド
public void visit(NodeB ノード) {
System.out.println(node.operationB());
}
}
抽象ノードクラスのコピーコードは次のとおりです。
パブリック抽象クラス ノード {
/**
* 操作を受け付けます
*/
public abstract void accept(訪問者 訪問者);
}
特定のノードクラス NodeA
次のようにコードをコピーします。
public class NodeA extends Node{
/**
* 操作を受け付けます
*/
@オーバーライド
public void accept(訪問者 訪問者) {
Visitor.visit(this);
}
/**
※NodeA固有の方法
*/
パブリック文字列操作A(){
「ノードA」を返します;
}
}
特定のノードクラス NodeB
次のようにコードをコピーします。
パブリック クラス NodeB extends Node{
/**
※受付方法
*/
@オーバーライド
public void accept(訪問者 訪問者) {
Visitor.visit(this);
}
/**
*NodeB 固有のメソッド
*/
パブリック文字列操作B(){
「ノードB」を返します。
}
}
構造オブジェクト ロール クラス。この構造オブジェクト ロールはコレクションを保持し、コレクションの管理操作として add() メソッドを外部に提供します。このメソッドを呼び出すことで、新しいノードを動的に追加できます。
次のようにコードをコピーします。
パブリック クラス ObjectStructure {
private List<Node> ノード = new ArrayList<Node>();
/**
* メソッド操作の実行
*/
public void アクション(訪問者 訪問者){
for(ノード ノード : ノード)
{
ノード.accept(訪問者);
}
}
/**
* 新しい要素を追加します
*/
public void add(Node ノード){
ノード.追加(ノード);
}
}
クライアント クラスのコピー コードは次のとおりです。
パブリック クラス クライアント {
public static void main(String[] args) {
//構造体オブジェクトを作成する
ObjectStructure os = new ObjectStructure();
// 構造体にノードを追加します
os.add(新しいNodeA());
// 構造体にノードを追加します
os.add(新しいNodeB());
// 訪問者を作成する
訪問者 Visitor = new VisitorA();
os.action(訪問者);
}
}
この回路図の実装では、複数のブランチ ノードを持つ複雑なオブジェクト ツリー構造は示されていませんが、実際のシステムでは、通常、ビジター パターンは複雑なオブジェクト ツリー構造を処理するために使用され、ビジター パターンは、複数の階層にまたがるツリー構造の問題を処理するために使用できます。 。ここで、訪問者のパターンが非常に強力になります。
準備工程シーケンス図
まず、この例示的なクライアントは構造オブジェクトを作成し、次に新しい NodeA オブジェクトと新しい NodeB オブジェクトを渡します。
次に、クライアントは VisitorA オブジェクトを作成し、このオブジェクトを構造オブジェクトに渡します。
次に、クライアントは構造オブジェクト集約管理メソッドを呼び出して、NodeA ノードと NodeB ノードを構造オブジェクトに追加します。
最後に、クライアントは構造体オブジェクトのアクション メソッド action() を呼び出してアクセス プロセスを開始します。
アクセス処理のシーケンス図
構造体オブジェクトは、保存するコレクション内のすべてのノード (このシステムではノード NodeA と NodeB) をトラバースします。まず、NodeA にアクセスします。このアクセスは次の操作で構成されます。
(1) NodeA オブジェクトの accept() メソッドが呼び出され、VisitorA オブジェクト自体が渡されます。
(2) NodeA オブジェクトは次に VisitorA オブジェクトのアクセス メソッドを呼び出し、NodeA オブジェクト自体を渡します。
(3)VisitorAオブジェクトは、NodeAオブジェクトの独自メソッドoperationA()を呼び出す。
これにより、二重ディスパッチ処理が完了し、次に、NodeBへのアクセス処理は、NodeAのアクセス処理と同様であるため、説明を省略する。
訪問者パターンの利点
● 優れた拡張性により、オブジェクト構造内の要素を変更することなく、オブジェクト構造内の要素に新しい機能を追加できます。
● 再利用性が高いため、オブジェクト構造全体に共通する機能を定義することができ、再利用性が向上します。
● 無関係な動作の分離 訪問者を使用して無関係な動作を分離し、関連する動作をカプセル化して訪問者を形成することができるため、各訪問者の機能は比較的単一になります。
訪問者パターンのデメリット
● オブジェクト構造を変更するのが難しい オブジェクト構造内のクラスが頻繁に変更される場合には、それに応じて訪問者のインターフェースや実装も変更する必要があり、コストがかかりすぎます。
● カプセル化の解除 訪問者パターンには、通常、内部データを訪問者に公開するためのオブジェクト構造と、オブジェクトのカプセル化を解除する ObjectStrutrue が必要です。