web-dev-qa-db-ja.com

訪問者パターンのaccept()メソッドのポイントは何ですか?

クラスからアルゴリズムを分離することについて多くの話があります。しかし、説明されていないことは一つあります。

彼らはこのように訪問者を使用します

_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()メソッドは不要であるというテクニックを示す論文をリンクすることにより、「ウォークアバウト」という用語を導入します。」

75
Val

ビジターパターンの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);
}

実行時に、rootactualタイプに関係なく、呼び出しはオーバーロード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の例では、TrainElementaccept()の実装を入力します。

class TrainNode extends Node implements IVisitable {
  void accept(IVisitor v) {
    v.visit(this);
  }
}

TrainNodeacceptのスコープ内で、コンパイラはこの時点で何を知っていますか? thisの静的型がTrainNodeであることを知っています。これは、コンパイラが呼び出し元のスコープ内で認識していなかった重要な追加情報です。そこで、rootについて知っているのは、それがNodeであるということだけでした。これで、コンパイラーはthisroot)が単なるNodeではなく、実際はTrainNodeであることを認識します。結果として、accept()内にある1行:v.visit(this)は、まったく別の何かを意味します。コンパイラは、TrainNodeをとるvisit()のオーバーロードを探します。見つからない場合は、Nodeを取るオーバーロードの呼び出しをコンパイルします。どちらも存在しない場合は、コンパイルエラーが発生します(objectを取るオーバーロードがない限り)。したがって、実行は、意図していたものに沿って開始されます:MyVisitorvisit(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()を必要とせずに訪問者を書くためのソリューションがありますが、それらは通常リフレクションを伴うため、非常に大きなオーバーヘッドが発生する可能性があります。

138
atanamir

もちろん、それが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ダブルディスパッチする場合は、ナビゲートする隣接ノードがまったくない特別な場合です。

15
George Mauer

Visitorパターンの目的は、オブジェクトがビジターが終了し、出発したことをオブジェクトに知らせ、クラスが必要なクリーンアップを後で実行できるようにすることです。また、クラスが内部を「一時的に」「ref」パラメータとして公開し、ビジターがいなくなると内部が公開されなくなることも認識できます。クリーンアップが必要ない場合、ビジターパターンはそれほど役に立ちません。これらのいずれも行わないクラスは、訪問者パターンの恩恵を受けない可能性がありますが、訪問者パターンを使用するために記述されたコードは、アクセス後にクリーンアップを必要とする将来のクラスで使用できます。

たとえば、アトミックに更新する必要のある多くの文字列を保持するデータ構造がありますが、データ構造を保持するクラスは、実行するアトミック更新の種類を正確に知りません(たとえば、1つのスレッドが「 X "、別のスレッドが数字のシーケンスを数値的に1つ高いシーケンスに置き換えたい場合、両方のスレッドの操作は成功するはずです。各スレッドが単に文字列を読み取り、更新を実行し、書き戻す場合、2番目のスレッドその文字列を書き戻すと、最初の文字列が上書きされます)。これを達成する1つの方法は、各スレッドにロックを取得させ、その操作を実行させ、ロックを解除することです。残念ながら、そのようにロックが公開されている場合、データ構造には、誰かがロックを取得してそれを解放することを防ぐ方法がありません。

Visitorパターンは、その問題を回避するために(少なくとも)3つのアプローチを提供します。

  1. レコードをロックし、提供された関数を呼び出してから、レコードのロックを解除できます。提供された関数が無限ループに陥ると、レコードは永久にロックされますが、提供された関数が例外を返すかスローすると、レコードはロック解除されますロックされていることはおそらく良い考えではありません)。呼び出された関数が他のロックを取得しようとすると、デッドロックが発生する可能性があることに注意してください。
  2. 一部のプラットフォームでは、文字列を保持する格納場所を「ref」パラメーターとして渡すことができます。次に、その関数は文字列をコピーし、コピーされた文字列に基づいて新しい文字列を計算し、古い文字列を新しい文字列と比較して、CompareExchangeが失敗した場合にプロセス全体を繰り返します。
  3. 文字列のコピーを作成し、文字列に対して指定された関数を呼び出し、CompareExchange自体を使用して元の更新を試み、CompareExchangeが失敗した場合はプロセス全体を繰り返すことができます。

訪問者パターンがない場合、アトミック更新を実行するには、呼び出しソフトウェアが厳密なロック/ロック解除プロトコルに従わない場合、ロックを公開し、失敗のリスクを負う必要があります。 Visitorパターンを使用すると、アトミック更新を比較的安全に実行できます。

0
supercat

変更が必要なクラスはすべて、「accept」メソッドを実装する必要があります。クライアントはこのacceptメソッドを呼び出して、そのクラスファミリで新しいアクションを実行し、機能を拡張します。クライアントは、この1つのacceptメソッドを使用して、特定のアクションごとに異なるビジタークラスを渡すことにより、幅広い新しいアクションを実行できます。訪問者クラスには、ファミリ内のすべてのクラスに対して同じ特定のアクションを達成する方法を定義する複数のオーバーライドされた訪問メソッドが含まれます。これらの訪問メソッドには、動作するインスタンスが渡されます。

機能の各項目は各ビジタークラスで個別に定義されており、クラス自体を変更する必要がないため、ビジターはクラスの安定したファミリに機能を頻繁に追加、変更、または削除する場合に役立ちます。クラスのファミリーが安定していない場合、多くの訪問者はクラスが追加または削除されるたびに変更する必要があるため、訪問者パターンはあまり使用されない可能性があります。

0
andrew pate