web-dev-qa-db-ja.com

ビジターパターンの代わりに?

ビジターパターンに代わるものを探しています。重要でない詳細をスキップしながら、パターンのいくつかの関連する側面に焦点を当てましょう。 Shapeの例を​​使用します(申し訳ありません!):

  1. IShapeインターフェースを実装するオブジェクトの階層があります
  2. 階層内のすべてのオブジェクトに対して実行されるグローバル操作がいくつかあります。 Draw、WriteToXmlなど...
  3. まっすぐに飛び込んで、Draw()メソッドとWriteToXml()メソッドをIShapeインターフェイスに追加するのは魅力的です。これは必ずしも良いことではありません。すべてのシェイプで実行される新しい操作を追加する場合は、IShapeから派生した各クラスを変更する必要があります。
  4. 各操作にビジターを実装します。つまり、DrawビジターまたはWirteToXmlビジターは、その操作のすべてのコードを1つのクラスにカプセル化します。新しい操作を追加するには、すべてのタイプのIShapeで操作を実行する新しいビジタークラスを作成します。
  5. 新しいIShape派生クラスを追加する必要がある場合、基本的に3で行ったのと同じ問題が発生します。新しいIShape派生タイプを処理するメソッドを追加するには、すべてのビジタークラスを変更する必要があります。

ビジターパターンについて読んだほとんどの場所では、ポイント5がパターンが機能するための主な基準であると述べており、私は完全に同意します。 IShapeから派生したクラスの数が固定されている場合、これは非常に洗練されたアプローチになります。

したがって、問題は、新しいIShape派生クラスが追加されたときです。各ビジター実装は、そのクラスを処理するために新しいメソッドを追加する必要があります。これは、せいぜい不快であり、最悪の場合、不可能であり、このパターンがそのような変更に対処するように実際に設計されていないことを示しています。

それで、問題は、誰かがこの状況を処理するための代替アプローチに出くわしたかどうかです。

50
Steg

戦略パターン を確認することをお勧めします。これにより、階層内の各クラスを変更せずに新しい機能を追加しながら、関心の分離を実現できます。

class AbstractShape
{
    IXmlWriter _xmlWriter = null;
    IShapeDrawer _shapeDrawer = null;

    public AbstractShape(IXmlWriter xmlWriter, 
                IShapeDrawer drawer)
    {
        _xmlWriter = xmlWriter;
        _shapeDrawer = drawer;
    }

    //...
    public void WriteToXml(IStream stream)
    {
        _xmlWriter.Write(this, stream);

    }

    public void Draw()
    {
        _drawer.Draw(this);
    }

    // any operation could easily be injected and executed 
    // on this object at run-time
    public void Execute(IGeneralStrategy generalOperation)
    {
        generalOperation.Execute(this);
    }
}

詳細については、この関連するディスカッションを参照してください。

オブジェクトはそれ自体をファイルに書き出す必要がありますか、それとも別のオブジェクトがそれに基づいてI/Oを実行する必要がありますか?

15
Dirk Vollmar

「デフォルトのビジターパターン」があります。このパターンでは、通常どおりビジターパターンを実行しますが、署名visitDefault(IShape)を使用してすべてを抽象メソッドに委任することにより、IShapeVisitorクラスを実装する抽象クラスを定義します。 。

次に、ビジターを定義するときに、インターフェースを直接実装するのではなく、この抽象クラスを拡張します。その時点で知っているvisit *メソッドをオーバーライドして、適切なデフォルトを提供できます。ただし、適切なデフォルトの動作を事前に把握する方法が実際にない場合は、インターフェイスを直接実装する必要があります。

新しいIShapeサブクラスを追加するときは、抽象クラスを修正してそのvisitDefaultメソッドに委任すると、デフォルトの動作を指定したすべての訪問者が新しいIShape

IShapeクラスが自然に階層に分類される場合のこれのバリエーションは、いくつかの異なるメソッドを介して抽象クラスをデリゲートすることです。たとえば、DefaultAnimalVisitorは次のようになります。

public abstract class DefaultAnimalVisitor implements IAnimalVisitor {
  // The concrete animal classes we have so far: Lion, Tiger, Bear, Snake
  public void visitLion(Lion l)   { visitFeline(l); }
  public void visitTiger(Tiger t) { visitFeline(t); }
  public void visitBear(Bear b)   { visitMammal(b); }
  public void visitSnake(Snake s) { visitDefault(s); }

  // Up the class hierarchy
  public void visitFeline(Feline f) { visitMammal(f); }
  public void visitMammal(Mammal m) { visitDefault(m); }

  public abstract void visitDefault(Animal a);
}

これにより、希望する特定のレベルで行動を指定する訪問者を定義できます。

残念ながら、訪問者が新しいクラスでどのように動作するかを指定するために何かをすることを避ける方法はありません-事前にデフォルトを設定できるか、できないかのどちらかです。 ( この漫画 の2番目のパネルも参照してください)

13
Daniel Martin

金属切削機用のCAD/CAMソフトウェアを管理しています。だから私はこの問題についていくらかの経験を持っています。

私たちが最初にソフトウェアを(1985年に最初にリリースされました!)オブジェクト指向設計に変換したとき、私はあなたが気に入らないことをしました。オブジェクトとインターフェイスには、Draw、WriteToFileなどがありました。変換の途中でデザインパターンを見つけて読むことは大いに役立ちましたが、それでも多くの悪いコードの臭いがありました。

やがて私は、これらのタイプの操作のどれも実際にはオブジェクトの関心事ではないことに気づきました。むしろ、さまざまな操作を実行するために必要なさまざまなサブシステム。これは、現在 パッシブビュー コマンドオブジェクトと呼ばれているものと、ソフトウェアのレイヤー間で明確に定義されたインターフェイスを使用して処理しました。

私たちのソフトウェアは基本的にこのように構成されています

  • さまざまなフォームインターフェイスを実装するフォーム。これらのフォームは、シェルがイベントをUIレイヤーに渡すものです。
  • イベントを受信し、フォームインターフェイスを介してフォームを操作するUIレイヤー。
  • UIレイヤーは、すべてコマンドインターフェイスを実装するコマンドを実行します
  • UIオブジェクトには、コマンドが対話できる独自のインターフェイスがあります。
  • コマンドは、必要な情報を取得して処理し、モデルを操作してからUIオブジェクトにレポートし、UIオブジェクトはフォームで必要なすべてのことを実行します。
  • 最後に、システムのさまざまなオブジェクトを含むモデル。シェイププログラム、カッティングパス、カッティングテーブル、金属シートなど。

したがって、描画はUIレイヤーで処理されます。マシンごとに異なるソフトウェアがあります。したがって、すべてのソフトウェアが同じモデルを共有し、同じコマンドの多くを再利用します。彼らは非常に異なる描画のようなものを扱います。たとえば、カッティングテーブルは、ルーターマシンとプラズマトーチを使用するマシンでは、どちらも本質的に巨大なX-Yフラットテーブルであるにもかかわらず、描画が異なります。これは、車のように2台のマシンが十分に異なって構築されているため、顧客にとって視覚的な違いがあるためです。

形は以下の通りです

入力したパラメータを介して切断パスを生成する形状プログラムがあります。切削経路は、どの形状プログラムが生成されたかを認識しています。ただし、切断経路は形状ではありません。画面に描いたり、形を切ったりするのに必要な情報だけです。この設計の理由の1つは、外部アプリからインポートするときに、シェイププログラムなしでカットパスを作成できることです。

この設計により、切断経路の設計を、必ずしも同じものではない形状の設計から分離することができます。あなたの場合、パッケージ化する必要があるのは、形状を描くために必要な情報だけです。

各シェイププログラムには、IShapeViewインターフェイスを実装する多数のビューがあります。 IShapeViewインターフェイスを介して、シェイププログラムは、一般的なシェイプフォームに、そのシェイプのパラメータを表示するように設定する方法を通知できます。汎用シェイプフォームはIShapeFormインターフェイスを実装し、ShapeScreenオブジェクトに登録します。 ShapeScreenオブジェクトは、それ自体をアプリケーションオブジェクトに登録します。シェイプビューは、アプリケーションに登録されているシェイプスクリーンを使用します。

さまざまな方法で形状を入力することを好む顧客がいるという複数のビューの理由。私たちの顧客ベースは、表形式で形状パラメーターを入力するのが好きな人と、その前に形状のグラフィック表現を入力するのが好きな人の間で半分に分かれています。また、フルシェイプの入力画面ではなく、最小限のダイアログからパラメータにアクセスする必要がある場合もあります。したがって、複数のビュー。

形状を操作するコマンドは、2つのカテゴリのいずれかに分類されます。切削経路を操作するか、形状パラメータを操作します。形状パラメータを操作するには、通常、形状入力画面に戻すか、最小限のダイアログを表示します。形状を再計算し、同じ場所に表示します。

切断パスについては、各操作を個別のコマンドオブジェクトにまとめました。たとえば、コマンドオブジェクトがあります

ResizePath RotatePath MovePathSplitPathなど。

新しい機能を追加する必要がある場合は、別のコマンドオブジェクトを追加し、右側のUI画面でメニュー、キーボードショートカット、またはツールバーボタンスロットを見つけて、そのコマンドを実行するようにUIオブジェクトを設定します。

例えば

   CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath

または

   CuttingTableScreen.Toolbar("Edit Path").AddButton Application.Icons("MirrorPath"),"Mirror Path", New MirrorPath

どちらの場合も、コマンドオブジェクトMirrorPathは目的のUI要素に関連付けられています。 MirrorPathのexecuteメソッドには、特定の軸のパスをミラーリングするために必要なすべてのコードがあります。コマンドに独自のダイアログがあるか、UI要素の1つを使用して、ミラーリングする軸をユーザーに尋ねる可能性があります。これは、訪問者を作成したり、パスにメソッドを追加したりするものではありません。

アクションをコマンドにバンドルすることで、多くのことを処理できることがわかります。ただし、それは黒または白の状況ではないことに注意してください。それでも、元のオブジェクトのメソッドとして特定のものがうまく機能することがわかります。経験を積むと、メソッドで行っていた作業の80%がコマンドに移動できることがわかりました。最後の20%は、オブジェクトに対して単純に機能します。

カプセル化に違反しているように見えるため、これを好まない人もいるかもしれません。私たちのソフトウェアを過去10年間オブジェクト指向システムとして維持してきたことから、あなたができる最も重要な長期的なことは、ソフトウェアのさまざまなレイヤー間およびさまざまなオブジェクト間の相互作用を明確に文書化することです。

アクションをコマンドオブジェクトにバンドルすることは、カプセル化の理想へのスラブな献身よりも、この目標に役立ちます。パスをミラーリングするために必要なことはすべて、パスのミラーリングコマンドオブジェクトにバンドルされています。

6
RS Conley

ビジターデザインパターンは回避策であり、問​​題の解決策ではありません。簡単な答えは パターンマッチング です。

4
Marko Tunjic

どのパスを使用するかに関係なく、Visitorパターンによって現在提供されている代替機能の実装は、それが機能しているインターフェースの具体的な実装について何かを「知る」必要があります。したがって、追加の実装ごとに追加の「ビジター」機能を作成する必要があるという事実を回避することはできません。つまり、あなたが探しているのは、この機能を作成するためのより柔軟で構造化されたアプローチです。

ビジター機能をシェイプのインターフェースから分離する必要があります。

私が提案するのは、ビジター機能の代替実装を作成するための抽象ファクトリを介した創造論的アプローチです。

public interface IShape {
  // .. common shape interfaces
}

//
// This is an interface of a factory product that performs 'work' on the shape.
//
public interface IShapeWorker {
     void process(IShape shape);
}

//
// This is the abstract factory that caters for all implementations of
// shape.
//
public interface IShapeWorkerFactory {
    IShapeWorker build(IShape shape);
    ...
}

//
// In order to assemble a correct worker we need to create
// and implementation of the factory that links the Class of
// shape to an IShapeWorker implementation.
// To do this we implement an abstract class that implements IShapeWorkerFactory
//
public AbsractWorkerFactory implements IShapeWorkerFactory {

    protected Hashtable map_ = null;

    protected AbstractWorkerFactory() {
          map_ = new Hashtable();
          CreateWorkerMappings();
    }

    protected void AddMapping(Class c, IShapeWorker worker) {
           map_.put(c, worker);
    }

    //
    // Implement this method to add IShape implementations to IShapeWorker
    // implementations.
    //
    protected abstract void CreateWorkerMappings();

    public IShapeWorker build(IShape shape) {
         return (IShapeWorker)map_.get(shape.getClass())
    }
}

//
// An implementation that draws circles on graphics
//
public GraphicsCircleWorker implements IShapeWorker {

     Graphics graphics_ = null;

     public GraphicsCircleWorker(Graphics g) {
        graphics_ = g;
     }

     public void process(IShape s) {
       Circle circle = (Circle)s;
       if( circle != null) {
          // do something with it.
          graphics_.doSomething();
       }
     }

}

//
// To replace the previous graphics visitor you create
// a GraphicsWorkderFactory that implements AbstractShapeFactory 
// Adding mappings for those implementations of IShape that you are interested in.
//
public class GraphicsWorkerFactory implements AbstractShapeFactory {

   Graphics graphics_ = null;
   public GraphicsWorkerFactory(Graphics g) {
      graphics_ = g;
   }

   protected void CreateWorkerMappings() {
      AddMapping(Circle.class, new GraphicCircleWorker(graphics_)); 
   }
}


//
// Now in your code you could do the following.
//
IShapeWorkerFactory factory = SelectAppropriateFactory();

//
// for each IShape in the heirarchy
//
for(IShape shape : shapeTreeFlattened) {
    IShapeWorker worker = factory.build(shape);
    if(worker != null)
       worker.process(shape);
}

それでも、「shape」の新しいバージョンで作業するには具体的な実装を作成する必要があることを意味しますが、shapeのインターフェイスから完全に分離されているため、元のインターフェイスとそれと相互作用するソフトウェアを壊すことなく、このソリューションを後付けできます。これは、IShapeの実装の周りの一種の足場として機能します。

2
Adrian Regan

Javaを使用している場合:はい、それはinstanceofと呼ばれます。人々はそれを使うことを過度に恐れています。ビジターパターンと比較すると、一般的に高速でわかりやすく、ポイント5に悩まされることはありません。

1
Andy

形状ごとに異なる動作をするn個のIShapesおよびm個の演算がある場合は、n * m個の個別の関数が必要です。これらすべてを同じクラスに入れることは、私にはひどい考えのように思えます。そして、あなたにある種の神オブジェクトを与えます。したがって、IShapeインターフェイスにm個の関数を1つずつ配置してIShapeでグループ化するか、n個の関数を配置して(ビジターパターンを使用して)操作でグループ化する必要があります。 、各操作/ビジタークラスのIShapeごとに1つ。

新しいIShapeを追加するとき、または新しい操作を追加するときに複数のクラスを更新する必要があるか、それを回避する方法はありません。


デフォルトのIShape関数を実装するための各操作を探している場合は、Daniel Martinの回答のように問題が解決します: https://stackoverflow.com/a/986034/1969638 、おそらくオーバーロードを使用しますが:

interface IVisitor
{
    void visit(IShape shape);
    void visit(Rectangle shape);
    void visit(Circle shape);
}

interface IShape
{
    //...
    void accept(IVisitor visitor);
}
1
Zantier

私は実際に次のパターンを使用してこの問題を解決しました。名前があるかどうかわからない!

public interface IShape
{
}

public interface ICircleShape : IShape
{
}

public interface ILineShape : IShape
{
}

public interface IShapeDrawer
{
    void Draw(IShape shape);

    /// <summary>
    /// Returns the type of the shape this drawer is able to draw!
    /// </summary>
    Type SourceType { get; }
}

public sealed class LineShapeDrawer : IShapeDrawer
{
    public Type SourceType => typeof(ILineShape);
    public void Draw(IShape drawing)
    {
        if (drawing is ILineShape)
        {
            // Code to draw the line
        }
    }
}

public sealed class CircleShapeDrawer : IShapeDrawer
{
    public Type SourceType => typeof(ICircleShape);
    public void Draw(IShape drawing)
    {
        if (drawing is ICircleShape)
        {
            // Code to draw the circle
        }
    }
}

public sealed class ShapeDrawingClient
{
    private readonly IDictionary<Type, IShapeDrawer> m_shapeDrawers =
        new Dictionary<Type, IShapeDrawer>();

    public void Add(IShapeDrawer shapeDrawer)
    {
        m_shapeDrawers[shapeDrawer.SourceType] = shapeDrawer;
    }

    public void Draw(IShape shape)
    {
        Type[] interfaces = shape.GetType().GetInterfaces();
        foreach (Type @interface in interfaces)
        {
            if (m_shapeDrawers.TryGetValue(@interface, out IShapeDrawer drawer))
              {
                drawer.Draw(drawing); 
                return;
              }

        }
    }
}

使用法:

        LineShapeDrawer lineShapeDrawer = new LineShapeDrawer();
        CircleShapeDrawer circleShapeDrawer = new CircleShapeDrawer();

        ShapeDrawingClient client = new ShapeDrawingClient ();
        client.Add(lineShapeDrawer);
        client.Add(circleShapeDrawer);

        foreach (IShape shape in shapes)
        {
            client.Draw(shape);
        }

これで、私のライブラリのユーザーとして誰かがIRectangleShapeを定義し、それを描画したい場合、彼らは単にIRectangleShapeDrawerを定義し、それをShapeDrawingClientのドロワーのリストに追加できます。

0
Vahid