56

訪問者パターンに代わるものを探しています。重要でない詳細は飛ばして、パターンのいくつかの関連する側面に焦点を当ててみましょう。Shape の例を使用します (申し訳ありません!):

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

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

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

では、問題は、この状況を処理するための別のアプローチに出くわした人はいますか?

4

8 に答える 8

15

Strategy パターンを参照してください。これにより、階層内の各クラスを変更することなく、新しい機能を追加できる一方で、懸念事項を分離できます。

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 を実行する必要がありますか?

于 2009-06-12T10:11:51.810 に答える
13

「デフォルトのビジター パターン」があります。この場合、ビジター パターンを通常どおりに実行しますIShapeVisitorが、シグネチャを持つ抽象メソッドにすべてを委譲することにより、クラスを実装する抽象クラスを定義しますvisitDefault(IShape)

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

新しいサブクラスを追加するときはIShape、抽象クラスをそのvisitDefaultメソッドに委譲するように修正します。デフォルトの動作を指定したすべての訪問者は、 new に対してその動作を取得します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 番目のパネルも参照してください)

于 2009-06-12T10:35:47.710 に答える
6

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

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

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

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

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

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

形は以下の通りです

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

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

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

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

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

カッティングパスでは、各操作を個別のコマンドオブジェクトにまとめました。たとえば、コマンドオブジェクトがあります

ResizePath RotatePathMovePathSplitPathなど。

新しい機能を追加する必要がある場合は、別のコマンドオブジェクトを追加し、右側の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年間オブジェクト指向システムとして維持してきたことから、あなたができる最も重要な長期的なことは、ソフトウェアのさまざまなレイヤー間およびさまざまなオブジェクト間の相互作用を明確に文書化することです。

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

于 2009-06-12T13:16:55.593 に答える
2

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

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

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

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 の実装に関する一種の足場として機能します。

于 2009-06-12T15:47:52.127 に答える
1

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

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


デフォルト関数を実装するための各操作を探している場合はIShape、ダニエル・マーティンの回答のように問題を解決できます: 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);
}
于 2015-06-26T13:57:31.107 に答える