4

はじめに

どうやら、私はプログラマーとしての人生全体で、「型にはまらない」ビジター パターンを実行してきたようです。

はい、Visitor のメソッドから具体的な複合要素の Visit メソッドにディスパッチしVisitます。

これが私が学んだ方法だと思いますが、今ではその例を見つけることができず、学んだソースがなくなりました。

さて、具体的な要素のディスパッチが複合要素のAcceptメソッドに組み込まれているという圧倒的な証拠に直面して、私が行ってきた方法に少なくとも何らかの利点があるかどうか疑問に思っています。私に見える2つの利点は次のとおりです。

  1. ディスパッチ方法を決定する場所は 1 か所しかありません。ベース ビジターです。
  2. 新しい複合要素タイプを追加して、基本ビジターにそれらを無視させることができますが、派生ビジターはVisitそれらを処理するためにオーバーライドできます。

基本的なコンポジット/ビジター モデルは次のとおりです。

// "Unorthodox" version
public class BaseVisitor 
{
    public virtual void Visit(CompositeElement e)
    {
         if(e is Foo)
         {
             VisitFoo((Foo)e);
         }
         else if(e is Bar)
         {             
             VisitBar((Bar)e);
         }
         else
         {
             VisitUnknown(e);
         }
    }

    protected virtual void VisitFoo(Foo foo) { }
    protected virtual void VisitBar(Bar bar) { }
    protected virtual void VisitUnknown(CompositeElement e) { }
} 

public class CompositeElement 
{
    public virtual void Accept(BaseVisitor visitor) { } 
}

public class Foo : CompositeElement { }
public class Bar : CompositeElement { }

訪問者クラスは、正規バージョンではなく、2 番目の型ベースのディスパッチを担当するようになったことに注意してください。たとえば、次のFooようになります。

// Canonical visitor pattern 2nd dispatch
public override void Accept(BaseVisitor visitor)
{
    visitor.VisitFoo(this);
}

さて、ディフェンスですが…

メリット1

新しい CompositeElement タイプを追加したいとしましょう:

public class Baz : CompositeElement { }

ビジター モデルでこの新しい要素タイプに対応するには、BaseVisitor クラスに変更を加えるだけです。

public class BaseVisitor 
{  
    public virtual void Visit(CompositeElement e)
    {
        // Existing cases elided...
        else if(e is Baz)
        {
            VisitBaz((Baz)e);
        }
    }

    protected virtual void VisitBaz(Foo foo) { }
}

if確かに、これは小さな問題ですが、メンテナンスが簡単になるようです (つまり、大きな問題やswitchステートメントを気にしない場合)。

メリット2

コンポジットを別のパッケージで拡張したいとしましょう。を変更せずにこれに対応できますBaseVisitor

public class ExtendedVisitor : BaseVisitor
{
    public override Visit(CompositeElement e)
    {
        if(e is ExtendedElement)
        {
            VisitExtended((ExtendedElement)e);
        }
        else
        {
            base.Visit(e);
        }            
    }

    protected virtual void VisitExtended(ExtendedElement e) { }
}

public class ExtendedCompositeElement : CompositeElement { }

この構造を持つことで、拡張された CompositeElement 型に対応するためにBaseVisitor必要な依存関係を断ち切ることができます。VisitExtended

結論

現時点では、Visitor パターンを十分に実装していないか、十分に長く維持していないため、不利な点が私に重くのしかかっています。明らかに、大きな switch ステートメントを維持するのは苦痛であり、パフォーマンスへの影響もありますが、BaseVisitor拡張機能に依存しないようにする柔軟性を上回るかどうかはわかりません。

マイナス面については、あなたの考えを考慮してください。

4

5 に答える 5

12

ビジター パターンが GoF book でそのまま定義されている主な理由は、C++ に実行時型識別 (RTTI) の形式がなかったからです。彼らは「ダブルディスパッチ」を使用して、ターゲットオブジェクトにそのタイプが何であるかを伝えました。かなりクールですが、説明するのが信じられないほど難しいトリックです。

あなたが説明したものと GoF Visitor パターン (あなたが言及したように) との主な違いは、明示的な「dispatch」メソッド - 引数の型をチェックして明示的な visitFoo、visitBar に送信する「visit」メソッドがあることです。などの方法。

GoF ビジター パターンは、データ オブジェクト自体を使用してディスパッチを行います。このメソッドは、向きを変えて「this」をビジターに返し、適切なメソッドに解決する「accept」メソッドを提供します。

すべてを 1 か所にまとめると、基本的な GoF パターンは次のようになります (私は Java 派なので、ここでは C# の代わりに Java コードを使用してください)

public interface Visitor {
    void visit(Type1 value1);
    void visit(Type2 value2);
    void visit(Type3 value3);
}

(必要に応じて、このインターフェイスをデフォルトのメソッド実装を持つ基本クラスにすることができることに注意してください)

データ オブジェクトはすべて「accept」メソッドを実装する必要があります。

public class Type1 {
    public void accept(Visitor v) {
        v.visit(this);
    }
}

注:これとGoFバージョンについて言及したこととの大きな違いは、メソッドのオーバーロードを使用できるため、「visit」メソッド名の一貫性が保たれることです。これにより、すべてのデータ オブジェクトが「accept」の同一の実装を持つことができ、タイプミスの可能性が減少します。

すべての型には、まったく同じメソッド コードが必要です。accept メソッドの「this」により、コンパイラは正しい visit メソッドに解決されます。

その後、必要に応じて Visitor インターフェイスを実装できます。

同じまたは異なるパッケージに新しいタイプ (たとえば Type4) を追加すると、説明した内容よりも変更が少なくて済むことに注意してください。同じパッケージ内にある場合、Visitor インターフェイス (および各実装) にメソッドを追加しますが、「dispatch」メソッドは必要ありません。

それは言った...

  • GoF の実装には、データ オブジェクトの連携/変更が必要です。これは、私が気に入らない主な点です (誰かに説明しようとすることは別として、それは非常に苦痛になる可能性があります。多くの人が「二重ディスパッチ」の概念に問題を抱えています)。私は自分のデータとそれで何をしようとしているのかを別々に保持することを非常に好みます-MVCタイプのアプローチ。
  • 実装と GoF 実装の両方で、新しい型を追加するためにコードを変更する必要があります。これにより、既存の訪問者の実装が壊れる可能性があります。
  • 実装も GoF 実装も静的です。特定のタイプの「何をすべきか」は、実行時に変更できません
  • 現在、最も頻繁に使用されている言語に RTTI があります。

ところで、私はジョンズ・ホプキンス大学でデザイン パターンを教えていますが、私が推奨したいのは、うまくダイナミックなアプローチです。

単純な単一オブジェクトの Visitor インターフェイスから始めます。

public interface Visitor<T> {
    void visit(T type);
}

次に、VisitorRegistry を作成します

public class VisitorRegistry {
    private Map<Class<?>, Visitor<?>> visitors = new HashMap<Class<?>, Visitor<?>>();
    public <T> void register(Class<T> clazz, Visitor<T> visitor) {
        visitors.put(clazz, visitor);
    }
    public <T> void visit(T thing) {
        // needs error checks, and possibly "walk up" to check supertypes if direct type not found
        // also -- can provide default action to perform - maybe register using Void.class?
        @SuppressWarnings("unchecked")
        Visitor<T> visitor = (Visitor<T>) visitors.get(thing.getClass());
        visitor.visit(thing);
    }
}

これを次のように使用します

VisitorRegistry registry = new VisitorRegistry();
registry.register(Person.class, new Visitor<Person>() {
    @Override public void visit(Person person) {
        System.out.println("I see " + person.getName());
    }});
// register other types similarly

// walk the data however you would...
for (Object thing : things) {
    registry.visit(thing);
}

これにより、アクセスしたいタイプごとに独立したビジターを登録できるようになり、新しいタイプが追加されるたびに既存のビジターの実装が壊れることはありません。

実行時にさまざまなビジターの組み合わせを再登録 (および登録解除) することもでき、構成情報から何をすべきかの定義をロードすることもできます。

お役に立てれば!

于 2010-11-05T20:21:02.787 に答える
4

非巡回ビジターパターンを見てください。switchそれはまた、大きな声明なしで、あなたがあなたの訪問者の適応にリストした利点を提供します:

// acyclic version 
public interface IBaseVisitor { }
public interface IBaseVisitor<T> : IBaseVisitor where T : CompositeElement {
  void Visit(T e) { }
}
public class CompositeElement {
  public virtual void Accept(IBaseVisitor visitor) { }
}
public class Foo : CompositeElement {
  public override void Accept(IBaseVisitor visitor) {
    if (visitor is IBaseVisitor<Foo>) {
      ((IBaseVisitor<Foo>)visitor).Visit(this);
    }
  }
}
public class Bar : CompositeElement {
  public override void Accept(IBaseVisitor visitor) {
    if (visitor is IBaseVisitor<Bar>) {
      ((IBaseVisitor<Bar>)visitor).Visit(this);
    }
  }
}

実際の訪問者は、訪問するサブクラスを選択できます。

public class MyVisitor : IBaseVisitor<Foo>, IBaseVisitor<Bar> {
  public void Visit(Foo e) { }
  public void Visit(Bar e) { }
}

階層内のタイプとビジター内のメソッドの間に循環依存関係がないため、「非循環」です。

于 2010-11-05T21:03:49.733 に答える
2

すでに述べた欠点(パフォーマンスと大きなswitchステートメントを維持する必要性)は別として、別の問題は、GoFビジターパターンで、CompositeElementの新しいサブクラスを追加すると、そのハンドラーを作成する必要があることです。 tもコンパイルします。一方、あなたのアプローチでは、新しいCompositeElementサブクラスを追加し、適切なビジターswitchステートメントを更新するのを忘れるのは簡単です。

特定の訪問者のクラスのサブセットのみを処理して、訪問者をサブクラス化するという提案は、これをさらに悪化させます。開発者がCompositeElementの新しいサブクラスを作成する場合、どのクラスが変更する必要があり、どのクラスを変更する必要がないかを知るために、既存のすべての訪問者クラスについての深い知識が必要になります。これは間違いが非常に簡単です。

于 2010-11-05T20:31:11.787 に答える
1

一部の言語には、これを非常に魅力のないものにする制限もあります。Javaには、インターフェースを介する場合を除いて、複数の継承はありません。すべての複合要素と訪問者に同じ基本クラスから派生するように要求すると、グロスタイプ階層が作成されます。

つまり、あなたのやり方では、VisitorとCompositeElementをインターフェースにすることはできません。

于 2010-11-05T20:37:00.370 に答える
0

私はvisitA、visitB、visitWhatever、acceptA、acceptB、acceptWhateverの実装は好きではありません。このアプローチは、階層にクラスを追加するたびにインターフェースを壊すことを意味するからです。

私がこれについて書いた記事を見てください。

この記事では、インターフェイスを壊さないポリモーフィックなケースなど、実際の例を使用して、すべてを詳細に説明しています。

于 2011-01-29T23:37:18.160 に答える