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


87

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

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

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()ます。「リフレクションが利用できる場合はメソッドは不要であり、テクニックに「ウォークアバウト」という用語を導入している」という論文をリンクしています。



「ビジターがacceptを呼び出すと、呼び出し先のタイプに基づいてcalがディスパッチされます。その後、呼び出し先はビジターのタイプ固有の訪問方法をコールバックし、このコールは実際のビジターのタイプに基づいてディスパッチされます。」言い換えれば、それは私を混乱させることを述べています。そのため、具体的に教えていただけますか?
Val

回答:


154

ビジターパターンの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呼び出しが過負荷になる場合visit(Node node)。これは、型で宣言されたすべての変数に当てはまりますNode。どうしてこれなの?Javaやその他のCに似た言語は、呼び出すオーバーロードを決定するときに、パラメーターの静的型、または変数が宣言されている型のみを考慮するためです。Javaは、実行時にすべてのメソッド呼び出しに対して、「オーケー、動的タイプはroot何ですか?ああ、わかりました。それはTrainNodeです。MyVisitorタイプのパラメーターを受け入れるメソッドがあるかどうかを確認するための追加の手順を実行しません。TrainNode... "。コンパイラは、コンパイル時に、呼び出されるメソッドを決定します(Javaが実際に引数の動的型を検査した場合、パフォーマンスはかなりひどいものになります。)

Javaは、メソッドが呼び出されたときにオブジェクトの実行時(つまり動的)タイプを考慮するための1つのツールを提供します-仮想メソッドディスパッチ。仮想メソッドを呼び出すと、その呼び出しは実際には関数ポインターで構成されるメモリ内のテーブルに送られます。各タイプにはテーブルがあります。特定のメソッドがクラスによってオーバーライドされた場合、そのクラスの関数テーブルエントリには、オーバーライドされた関数のアドレスが含まれます。クラスがメソッドをオーバーライドしない場合、基本クラスの実装へのポインターが含まれます。これでもパフォーマンスのオーバーヘッドが発生します(各メソッド呼び出しは基本的に2つのポインターを逆参照します。1つは型の関数テーブルを指し、もう1つは関数自体を指します)が、パラメーター型を検査するよりも高速です。

ビジターパターンの目標は、ダブルディスパッチを達成することです-呼び出しターゲットのタイプが考慮されるだけでなく(MyVisitor仮想メソッドを介して)、パラメーターのタイプ(どのタイプNodeを見ているのか)も考慮されますか?ビジターパターンでは、visit/のaccept組み合わせでこれを行うことができます。

私たちの行をこれに変更することによって:

root.accept(new MyVisitor());

必要なものを取得できます。仮想メソッドディスパッチを介して、サブクラスによって実装された正しいaccept()呼び出しを入力します。この例ではTrainElementTrainElementの実装を入力しますaccept()

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)、まったく別のことを意味します。コンパイラは、今の過負荷を探しますvisit()とりますTrainNode。見つからない場合は、オーバーロードへの呼び出しをコンパイルします。Node。どちらも存在しない場合は、コンパイルエラーが発生します(過負荷がかかる場合を除くobject)。したがって、実行は、私たちがずっと意図していたものに入ります:MyVisitorの実装visit(TrainNode e)。キャストは必要ありませんでした、そして最も重要なことに、反射は必要ありませんでした。したがって、このメカニズムのオーバーヘッドはかなり低く、ポインタ参照のみで構成され、他には何も含まれていません。

あなたはあなたの質問に正しいです-私たちはキャストを使用して正しい振る舞いを得ることができます。ただし、多くの場合、ノードのタイプがわからないことがあります。次の階層の場合を考えてみましょう。

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;
  }
}

我々はの種類がわからないので、キャストすると、非常に遠く、私たちを取得していないでしょうleftrightにおけるvisit()方法を。私たちのパーサーはNode、階層のルートを指すタイプのオブジェクトも返す可能性が高いため、それを安全にキャストすることもできません。したがって、単純なインタプリタは次のようになります。

Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);

ビジターパターンを使用すると、非常に強力な処理を実行できます。オブジェクト階層が与えられると、階層のクラス自体にコードを配置しなくても、階層上で動作するモジュラー操作を作成できます。ビジターパターンは、コンパイラーの構築などで広く使用されています。特定のプログラムの構文ツリーを考えると、そのツリーを操作する多くの訪問者が作成されます。型チェック、最適化、マシンコードの発行はすべて、通常、異なる訪問者として実装されます。最適化ビジターの場合、入力ツリーを指定して新しい構文ツリーを出力することもできます。

もちろん、欠点もあります。階層に新しい型を追加する場合はvisit()、その新しい型のメソッドもIVisitorインターフェイスに追加し、すべての訪問者にスタブ(または完全)実装を作成する必要があります。accept()上記の理由から、メソッドも追加する必要があります。パフォーマンスがそれほど重要でない場合はaccept()、を必要とせずに訪問者を書き込むためのソリューションがありますが、通常はリフレクションを伴うため、かなり大きなオーバーヘッドが発生する可能性があります。


5
効果的なJavaアイテム#41には、次の警告が含まれています。キャストを追加することにより、同じパラメータセットが異なるオーバーロードに渡される可能性がある状況を回避します。accept()この警告がVisitorで違反された場合、メソッドが必要になります。
jaco0646 2015

通常、ビジターパターンが使用される場合、すべてのノードが基本ノードタイプから派生するオブジェクト階層が含まれます」これはC ++では絶対に必要ありません。参照Boost.Variant、Eggs.Variant
ジャン-マイケルCelerier

Javaでは常に最も具体的な型メソッドを呼び出すため、Javaではacceptメソッドは実際には必要ないようです
Gilad Baruchian 2017

1
うわー、これは素晴らしい説明でした。パターンのすべての影がコンパイラの制限によるものであることを確認するための啓発、そして今あなたのおかげで明確に明らかになります。
アルフォンソ西川

@GiladBaruchianの場合、コンパイラーは、コンパイラーが判別できる最も具体的な型メソッドへの呼び出しを生成します。
MMW

15

もちろん、それがAcceptを実装する唯一の方法であるとしたら、それはばかげているでしょう。

そうではありません。

たとえば、訪問者は階層を扱うときに本当に便利です。その場合、非終端ノードの実装は次のようになります。

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;
  ....
}

分かりますか?あなたが愚かだと言うのは階層を横断するため解決策です。

これは私に訪問者を理解させたはるかに長くて詳細な記事です

編集: 明確にするために:訪問者のVisitメソッドには、ノードに適用されるロジックが含まれています。ノードのAcceptメソッドには、隣接するノードに移動する方法に関するロジックが含まれています。ダブルディスパッチのみの場合は、ナビゲートする隣接ノードがないという特殊なケースです。


7
あなたの説明は、子供たちを反復するために訪問者の適切なvisit()メソッドではなくノードの責任である必要がある理由を説明していませんか?異なる訪問者に同じ訪問パターンが必要な場合、主なアイデアは階層トラバーサルコードを共有することだということですか?おすすめの論文からのヒントは見当たりません。
Val

1
受け入れることが日常的なトラバーサルに適していると言うことは、一般の人々にとって合理的で価値があります。しかし、私は誰かの「andymaleh.blogspot.com/2008/04/…を読むまでビジターパターンを理解できませんでした」から私の例を取りました。この例もウィキペディアも他の回答も、ナビゲーションの利点については言及していません。それにもかかわらず、彼らは皆、この愚かなaccept()を要求します。それが私の質問をする理由です:なぜですか?
Val

1
@ Val-どういう意味ですか?何を求めているのかわかりません。他の記事については、これらの人々の見方が異なるため、話すことはできませんが、私たちが意見の相違を持っているとは思えません。一般に、計算では多くの問題をネットワークにマッピングできるため、使用法は表面のグラフとは関係がない場合がありますが、実際には非常によく似た問題です。
ジョージマウアー2012

1
いくつかの方法が役立つ可能性がある場所の例を提供しても、その方法が必須である理由の質問には答えられません。ナビゲーションは必ずしも必要ではないため、accept()メソッドは必ずしも訪問に適しているとは限りません。したがって、それなしで目標を達成できるはずです。それにもかかわらず、それは必須です。これは、すべてのビジターパターンにaccept()を導入する理由が、「時々役立つ」よりも強いことを意味します。私の質問で明確でないことは何ですか?ウィキペディアが受け入れを取り除く方法を探す理由を理解しようとしないのであれば、私の質問を理解することに興味はありません。
Val

1
@Val彼らが「ビジターパターンの本質」にリンクしている論文は、私が与えたのと同じ要約でのナビゲーションと操作の分離に言及しています。彼らは単に、GOFの実装(あなたが求めているもの)には、リフレクションを使用して取り除くことができるいくつかの制限と煩わしさがあると言っているので、ウォークアバウトパターンを導入します。これは確かに便利で、訪問者ができることと同じことの多くを行うことができますが、かなり洗練されたコードがたくさんあり、(大雑把に読んで)型安全性のいくつかの利点を失います。これはツールボックス用のツールですが、訪問者よりも重いツールです
George Mauer 2012

0

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

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

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

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

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


2
1.訪問とは、訪問したパブリックメソッドにのみアクセスできることを意味します。そのため、訪問者が役立つように、パブリックが内部ロックにアクセスできるようにする必要があります。2 /私が以前に見た例のどれも、Visitorがvisitedのステータスを変更するために使用されることになっていることを意味しません。3.「従来のVisitorPatternでは、いつノードに入るのかしか判断できません。現在のノードに入る前に前のノードを離れたかどうかはわかりません。」visitEnterとvisitLeaveの代わりにvisitだけでロックを解除するにはどうすればよいですか?最後に、Visitorではなくaccpet()のアプリケーションについて質問しました。
Val

おそらく私はパターンの用語に完全に精通しているわけではありませんが、「ビジターパターン」は、XがYにデリゲートを渡し、Yが情報を渡すことができるという私が使用したアプローチに似ているようです。デリゲートが実行されている限り。たぶん、そのパターンには他の名前がありますか?
スーパーキャット2012

2
これは、特定の問題に対するビジターパターンの興味深いアプリケーションですが、パターン自体を説明したり、元の質問に答えたりするものではありません。「クリーンアップが必要ない場合、ビジターパターンはそれほど役に立ちません。」この主張は間違いなく誤りであり、特定の問題にのみ関係し、一般的なパターンには関係しません。
Tony O'Hagan 2017年

0

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

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


-1

良い例では、ソースコードのコンパイルです。

interface CompilingVisitor {
   build(SourceFile source);
}

クライアントが実装することができJavaBuilderRubyBuilderXMLValidator、など、プロジェクト内のすべてのソースファイルを収集し、訪問の実装は変更する必要はありません。

ソースファイルの種類ごとに別々のクラスがある場合、これは悪いパターンになります。

interface CompilingVisitor {
   build(JavaSourceFile source);
   build(RubySourceFile source);
   build(XMLSourceFile source);
}

それは、コンテキストと、システムのどの部分を拡張可能にしたいのかということになります。


皮肉なことに、VisitorPatternは悪いパターンを使用するように提案しています。訪問するノードの種類ごとに訪問方法を定義する必要があると書かれています。第二に、あなたの例が良いか悪いかは明確ではありませんか?それらは私の質問とどのように関連していますか?
Val
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.