クラスからアルゴリズムを分離することについて多くの話があります。しかし、説明されていないことは一つあります。
彼らはこのように訪問者を使用します
_abstract class Expr {
public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}
class ExprVisitor extends Visitor{
public Integer visit(Num num) {
return num.value;
}
public Integer visit(Sum sum) {
return sum.getLeft().accept(this) + sum.getRight().accept(this);
}
public Integer visit(Prod prod) {
return prod.getLeft().accept(this) * prod.getRight().accept(this);
}
_
Visit(element)を直接呼び出す代わりに、Visitorは要素にvisitメソッドを呼び出すように要求します。これは、訪問者についてクラスを意識しないという宣言されたアイデアと矛盾します。
PS1あなた自身の言葉で説明するか、正確な説明を指してください。私が得た2つの応答は、一般的で不確実なものを参照しているためです。
PS2推測:getLeft()
は基本的なExpression
を返すので、visit(getLeft())
を呼び出すとvisit(Expression)
になりますが、getLeft()
を呼び出すとvisit(this)
は、より適切な別の訪問呼び出しをもたらします。したがって、accept()
は型変換(別名キャスト)を実行します。
PS3 Scalaのパターンマッチング=ステロイドの訪問者パターン は、Acceptメソッドを使用しない場合の訪問者パターンの単純さを示しています。 ウィキペディアはこの声明に追加 :「リフレクションが利用可能な場合、accept()
メソッドは不要であるというテクニックを示す論文をリンクすることにより、「ウォークアバウト」という用語を導入します。」
ビジターパターンのvisit
/accept
コンストラクトは、Cに似た言語(C#、Javaなど)のセマンティクスのために必要な悪です。ビジターパターンの目標は、コードを読むことから予想されるとおり、ダブルディスパッチを使用してコールをルーティングすることです。
通常、訪問者パターンを使用する場合、すべてのノードがベースNode
タイプ(以降Node
と呼ばれる)から派生するオブジェクト階層が含まれます。直感的に、次のように記述します。
Node root = GetTreeRoot();
new MyVisitor().visit(root);
ここに問題があります。 MyVisitor
クラスが次のように定義されている場合:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
実行時に、root
のactualタイプに関係なく、呼び出しはオーバーロードvisit(Node node)
になります。これは、Node
型で宣言されたすべての変数に当てはまります。どうしてこれなの? Javaおよび他のCライクな言語は、決定するときのパラメーターのstatic type、または変数が宣言されている型のみを考慮するため、どのオーバーロードを呼び出すか。Javaは、実行時にすべてのメソッド呼び出しに対して、「わかりました、root
の動的な型は何ですか?ああ、わかりました。 TrainNode
。タイプMyVisitor
のパラメーターを受け入れるメソッドがTrainNode
にあるかどうかを見てみましょう。」コンパイラーは、コンパイル時に、どのメソッドが呼び出されるかを決定します。(If Java実際に引数の動的型を検査したので、パフォーマンスはかなりひどいものになりました。)
Javaは、メソッドが呼び出されたときにオブジェクトのランタイム(動的)タイプを考慮するための1つのツールを提供します- 仮想メソッドディスパッチ 。仮想メソッドを呼び出すと、実際には、関数ポインターで構成されるメモリ内の table に呼び出しが行われます。各タイプにはテーブルがあります。特定のメソッドがクラスによってオーバーライドされると、そのクラスの関数テーブルエントリにはオーバーライドされた関数のアドレスが含まれます。クラスがメソッドをオーバーライドしない場合、基本クラスの実装へのポインターが含まれます。これは依然としてパフォーマンスのオーバーヘッドを招きます(各メソッド呼び出しは基本的に2つのポインターを逆参照します:1つは型の関数テーブルを指し、もう1つは関数自体を指します)が、パラメーター型を検査するよりも高速です。
訪問者パターンの目標は、達成することです double-dispatch -呼び出しターゲットのタイプ(MyVisitor
、仮想メソッド経由)だけでなく、パラメーターのタイプ(どのタイプのNode
を見ていますか?) Visitorパターンを使用すると、visit
/accept
の組み合わせでこれを行うことができます。
行をこれに変更することにより:
root.accept(new MyVisitor());
必要なものを取得できます。仮想メソッドディスパッチを介して、サブクラスによって実装された正しいaccept()呼び出しを入力します-TrainElement
の例では、TrainElement
のaccept()
の実装を入力します。
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
TrainNode
のaccept
のスコープ内で、コンパイラはこの時点で何を知っていますか? this
の静的型がTrainNode
であることを知っています。これは、コンパイラが呼び出し元のスコープ内で認識していなかった重要な追加情報です。そこで、root
について知っているのは、それがNode
であるということだけでした。これで、コンパイラーはthis
(root
)が単なるNode
ではなく、実際はTrainNode
であることを認識します。結果として、accept()
内にある1行:v.visit(this)
は、まったく別の何かを意味します。コンパイラは、TrainNode
をとるvisit()
のオーバーロードを探します。見つからない場合は、Node
を取るオーバーロードの呼び出しをコンパイルします。どちらも存在しない場合は、コンパイルエラーが発生します(object
を取るオーバーロードがない限り)。したがって、実行は、意図していたものに沿って開始されます:MyVisitor
のvisit(TrainNode e)
の実装。キャストは不要であり、最も重要なこととして、反射は必要ありませんでした。したがって、このメカニズムのオーバーヘッドはかなり低く、ポインタ参照のみで構成され、他には何もありません。
あなたはあなたの質問に正しいです-キャストを使用して正しい動作を得ることができます。ただし、多くの場合、タイプNodeが何であるかさえわかりません。次の階層の場合を考えます。
abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }
そして、ソースファイルを解析し、上記の仕様に準拠したオブジェクト階層を生成する単純なコンパイラを作成していました。訪問者として実装された階層のインタープリターを作成している場合:
class Interpreter implements IVisitor<int> {
int visit(AdditionNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left + right;
}
int visit(MultiplicationNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left * right;
}
int visit(LiteralNode n) {
return n.value;
}
}
visit()
メソッドのleft
またはright
のタイプがわからないため、キャストはそれほど遠くなりません。パーサーは、階層のルートも指し示したNode
型のオブジェクトを返す可能性が高いため、安全にキャストすることもできません。したがって、単純なインタープリターは次のようになります。
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
ビジターパターンを使用すると、非常に強力なことができます。オブジェクト階層を指定すると、コードを階層のクラス自体に配置する必要なく、階層を操作するモジュラー操作を作成できます。ビジターパターンは、たとえば、コンパイラの構築で広く使用されています。特定のプログラムの構文ツリーを考えると、そのツリーで動作する多くのビジターが書かれています。タイプチェック、最適化、マシンコードの発行は通常、すべて異なるビジターとして実装されます。最適化ビジターの場合、入力ツリーを指定して新しい構文ツリーを出力することもできます。
もちろん、欠点があります。新しい型を階層に追加する場合、その新しい型のvisit()
メソッドもIVisitor
インターフェイスに追加し、すべてのスタブ(または完全な)実装を作成する必要があります訪問者の。上記の理由により、accept()
メソッドも追加する必要があります。パフォーマンスがあなたにとってそれほど意味がない場合、accept()
を必要とせずに訪問者を書くためのソリューションがありますが、それらは通常リフレクションを伴うため、非常に大きなオーバーヘッドが発生する可能性があります。
もちろん、それがAcceptが実装されているonly方法であれば、それはばかげているでしょう。
そうではありません。
たとえば、階層を扱う場合、訪問者は本当に本当に便利です。この場合、非終端ノードの実装は次のようになります。
interface IAcceptVisitor<T> {
void Accept(IVisit<T> visitor);
}
class HierarchyNode : IAcceptVisitor<HierarchyNode> {
public void Accept(IVisit<T> visitor) {
visitor.visit(this);
foreach(var n in this.children)
n.Accept(visitor);
}
private IEnumerable<HierarchyNode> children;
....
}
分かりますか?あなたが愚かだと言うのは、階層を横断するためのソリューションです。
これは、訪問者を理解させてくれた、より長く詳細な記事です 。
Edit:明確にするために:訪問者のVisit
メソッドには、ノードに適用されるロジックが含まれています。ノードのAccept
メソッドには、隣接ノードへのナビゲート方法に関するロジックが含まれています。あなたがonlyダブルディスパッチする場合は、ナビゲートする隣接ノードがまったくない特別な場合です。
Visitorパターンの目的は、オブジェクトがビジターが終了し、出発したことをオブジェクトに知らせ、クラスが必要なクリーンアップを後で実行できるようにすることです。また、クラスが内部を「一時的に」「ref」パラメータとして公開し、ビジターがいなくなると内部が公開されなくなることも認識できます。クリーンアップが必要ない場合、ビジターパターンはそれほど役に立ちません。これらのいずれも行わないクラスは、訪問者パターンの恩恵を受けない可能性がありますが、訪問者パターンを使用するために記述されたコードは、アクセス後にクリーンアップを必要とする将来のクラスで使用できます。
たとえば、アトミックに更新する必要のある多くの文字列を保持するデータ構造がありますが、データ構造を保持するクラスは、実行するアトミック更新の種類を正確に知りません(たとえば、1つのスレッドが「 X "、別のスレッドが数字のシーケンスを数値的に1つ高いシーケンスに置き換えたい場合、両方のスレッドの操作は成功するはずです。各スレッドが単に文字列を読み取り、更新を実行し、書き戻す場合、2番目のスレッドその文字列を書き戻すと、最初の文字列が上書きされます)。これを達成する1つの方法は、各スレッドにロックを取得させ、その操作を実行させ、ロックを解除することです。残念ながら、そのようにロックが公開されている場合、データ構造には、誰かがロックを取得してそれを解放することを防ぐ方法がありません。
Visitorパターンは、その問題を回避するために(少なくとも)3つのアプローチを提供します。
訪問者パターンがない場合、アトミック更新を実行するには、呼び出しソフトウェアが厳密なロック/ロック解除プロトコルに従わない場合、ロックを公開し、失敗のリスクを負う必要があります。 Visitorパターンを使用すると、アトミック更新を比較的安全に実行できます。
変更が必要なクラスはすべて、「accept」メソッドを実装する必要があります。クライアントはこのacceptメソッドを呼び出して、そのクラスファミリで新しいアクションを実行し、機能を拡張します。クライアントは、この1つのacceptメソッドを使用して、特定のアクションごとに異なるビジタークラスを渡すことにより、幅広い新しいアクションを実行できます。訪問者クラスには、ファミリ内のすべてのクラスに対して同じ特定のアクションを達成する方法を定義する複数のオーバーライドされた訪問メソッドが含まれます。これらの訪問メソッドには、動作するインスタンスが渡されます。
機能の各項目は各ビジタークラスで個別に定義されており、クラス自体を変更する必要がないため、ビジターはクラスの安定したファミリに機能を頻繁に追加、変更、または削除する場合に役立ちます。クラスのファミリーが安定していない場合、多くの訪問者はクラスが追加または削除されるたびに変更する必要があるため、訪問者パターンはあまり使用されない可能性があります。