it-swarm.com.de

Alternative zum Besuchermuster?

Ich suche eine Alternative zum Besuchermuster. Lassen Sie mich nur auf einige relevante Aspekte des Musters eingehen und dabei unwichtige Details überspringen. Ich werde ein Shape-Beispiel verwenden (sorry!):

  1. Sie haben eine Hierarchie von Objekten, die die IShape-Schnittstelle implementieren
  2. Sie haben eine Reihe von globalen Operationen, die für alle Objekte in der Hierarchie ausgeführt werden sollen, z. Zeichnen, WriteToXml etc ...
  3. Es ist verlockend, direkt in die IShape-Oberfläche einzutauchen und eine Draw () - und WriteToXml () -Methode hinzuzufügen. Dies ist nicht unbedingt eine gute Sache. Wenn Sie eine neue Operation hinzufügen möchten, die für alle Shapes ausgeführt werden soll, muss jede von IShape abgeleitete Klasse geändert werden
  4. Das Implementieren eines Besuchers für jede Operation, d. H. Eines Draw-Besuchers oder eines WirteToXml-Besuchers, kapselt den gesamten Code für diese Operation in einer Klasse. Beim Hinzufügen eines neuen Vorgangs muss dann eine neue Besucherklasse erstellt werden, die den Vorgang für alle IShape-Typen ausführt
  5. Wenn Sie eine neue von IShape abgeleitete Klasse hinzufügen müssen, haben Sie im Wesentlichen das gleiche Problem wie in 3 - alle Besucherklassen müssen geändert werden, um eine Methode für die Behandlung des neuen von IShape abgeleiteten Typs hinzuzufügen

Die meisten Stellen, an denen Sie über das Besuchermuster lesen, geben an, dass Punkt 5 so ziemlich das Hauptkriterium für das Funktionieren des Musters ist, und ich stimme vollkommen zu. Wenn die Anzahl der von IShape abgeleiteten Klassen festgelegt ist, kann dies ein recht eleganter Ansatz sein.

Das Problem ist also, dass beim Hinzufügen einer neuen, von IShape abgeleiteten Klasse für jede Besucherimplementierung eine neue Methode hinzugefügt werden muss, um diese Klasse zu verarbeiten. Dies ist bestenfalls unangenehm und schlimmstenfalls nicht möglich und zeigt, dass dieses Muster nicht wirklich dafür ausgelegt ist, mit solchen Veränderungen umzugehen.

Die Frage ist also, ob jemand auf alternative Ansätze zur Bewältigung dieser Situation gestoßen ist.

50
Steg

Vielleicht möchten Sie einen Blick auf das Strategiemuster werfen. Dies gibt Ihnen immer noch eine Trennung der Bedenken, während Sie dennoch neue Funktionen hinzufügen können, ohne jede Klasse in Ihrer Hierarchie ändern zu müssen.

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

Weitere Informationen finden Sie in dieser Diskussion:

Sollte sich ein Objekt in eine Datei ausschreiben, oder sollte ein anderes Objekt darauf einwirken, um E/A durchzuführen?

15
Dirk Vollmar

Es gibt das "Besuchermuster mit Standard", bei dem Sie das Besuchermuster wie gewohnt ausführen, dann aber eine abstrakte Klasse definieren, die Ihre Klasse IShapeVisitor implementiert, indem Sie alles an eine abstrakte Methode mit der Signatur visitDefault(IShape) delegieren.

Wenn Sie dann einen Besucher definieren, erweitern Sie diese abstrakte Klasse, anstatt die Schnittstelle direkt zu implementieren. Sie können die visit * -Methode überschreiben, die Sie zu diesem Zeitpunkt kennen, und einen vernünftigen Standardwert festlegen. Wenn es jedoch nicht möglich ist, das vernünftige Standardverhalten im Voraus herauszufinden, sollten Sie die Schnittstelle direkt implementieren.

Wenn Sie eine neue IShape-Unterklasse hinzufügen, reparieren Sie die abstrakte Klasse, um sie an ihre visitDefault-Methode zu delegieren, und jeder Besucher, der ein Standardverhalten angegeben hat, erhält dieses Verhalten für die neue IShape.

Eine Variation davon, wenn Ihre Klassen IShape auf natürliche Weise in eine Hierarchie fallen, besteht darin, die abstrakte Klasse durch verschiedene Methoden delegieren zu lassen. Zum Beispiel könnte eine DefaultAnimalVisitor Folgendes tun:

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

Auf diese Weise können Sie Besucher definieren, die ihr Verhalten auf einer von Ihnen gewünschten Ebene festlegen.

Leider können Sie nicht umhin, festzulegen, wie sich Besucher mit einer neuen Klasse verhalten. Entweder können Sie im Voraus eine Standardeinstellung festlegen, oder Sie können dies nicht. (Siehe auch die zweite Tafel von diese Karikatur )

13
Daniel Martin

Ich unterhalte eine CAD/CAM-Software für Zerspanungsmaschinen. Ich habe also einige Erfahrungen mit diesen Problemen.

Als wir unsere Software zum ersten Mal (sie wurde 1985 veröffentlicht!) Auf ein objektorientiertes Design umstellten, tat ich genau das, was Ihnen nicht gefällt. Objekte und Schnittstellen hatten Draw, WriteToFile usw. Das Erkennen und Lesen von Entwurfsmustern auf halbem Weg durch die Konvertierung hat viel geholfen, aber es gab immer noch viele schlechte Codegerüche.

Irgendwann wurde mir klar, dass keine dieser Operationen das eigentliche Anliegen des Objekts war. Sondern die verschiedenen Subsysteme, die für die verschiedenen Operationen benötigt wurden. Ich handhabte dies mit einem sogenannten Passive View Command-Objekt und einer gut definierten Schnittstelle zwischen den Softwareschichten.

Unsere Software ist grundsätzlich so aufgebaut

  • Die Formulare implementieren verschiedene Formularschnittstellen. Bei diesen Formularen wird Shell Ereignisse an die Benutzeroberflächenebene übergeben.
  • Benutzeroberflächenebene, die Ereignisse empfängt und Formulare über die Formularschnittstelle bearbeitet.
  • Die UI-Ebene führt Befehle aus, die alle die Befehlsschnittstelle implementieren
  • Das UI-Objekt verfügt über eigene Schnittstellen, mit denen der Befehl interagieren kann.
  • Die Befehle erhalten die benötigten Informationen, verarbeiten sie, bearbeiten das Modell und melden sie dann an die UI-Objekte zurück, die dann alle erforderlichen Aktionen mit den Formularen ausführen.
  • Schließlich die Modelle, die die verschiedenen Objekte unseres Systems enthalten. Wie Formprogramme, Schneidpfade, Schneidetische und Bleche.

Das Zeichnen wird also in der UI-Ebene behandelt. Wir haben unterschiedliche Software für unterschiedliche Maschinen. Während also alle unsere Software das gleiche Modell haben und viele der gleichen Befehle wiederverwenden. Sie gehen mit Dingen wie Zeichnen ganz anders um. Beispielsweise unterscheidet sich ein Schneidetisch für eine Fräsmaschine von einer Maschine mit Plasmabrenner, obwohl beide im Wesentlichen ein riesiger X-Y-Flachtisch sind. Das liegt daran, dass die beiden Maschinen wie Autos so unterschiedlich gebaut sind, dass ein optischer Unterschied zum Kunden besteht.

Was Formen betrifft, so tun wir Folgendes

Wir haben Formprogramme, die Schnittpfade durch die eingegebenen Parameter erzeugen. Der Schneidweg weiß, welches Formprogramm erzeugt wurde. Ein Schnittpfad ist jedoch keine Form. Es werden nur die Informationen benötigt, um auf dem Bildschirm zu zeichnen und die Form zu schneiden. Ein Grund für dieses Design ist, dass Schnittpfade ohne ein Formprogramm erstellt werden können, wenn sie aus einer externen App importiert werden.

Diese Konstruktion ermöglicht es uns, die Konstruktion des Schneidweges von der Konstruktion der Form zu trennen, die nicht immer dasselbe sind. In Ihrem Fall müssen Sie wahrscheinlich nur die Informationen verpacken, die zum Zeichnen der Form erforderlich sind.

Jedes Formprogramm verfügt über eine Reihe von Ansichten, die eine IShapeView-Schnittstelle implementieren. Über die IShapeView-Oberfläche kann das Formprogramm die generische Form angeben, die wir einrichten müssen, um die Parameter dieser Form anzuzeigen. Das generische Shape-Formular implementiert eine IShapeForm-Schnittstelle und registriert sich beim ShapeScreen-Objekt. Das ShapeScreen-Objekt registriert sich bei unserem Anwendungsobjekt. In den Formansichten wird der Shapescreen verwendet, der sich bei der Anwendung registriert.

Der Grund für die Vielzahl von Ansichten, die Kunden haben, die Formen auf unterschiedliche Weise eingeben möchten. Unser Kundenstamm ist in zwei Hälften aufgeteilt: diejenigen, die Formparameter in einer Tabellenform eingeben möchten, und diejenigen, die eine grafische Darstellung der Form vor sich eingeben möchten. Wir müssen manchmal auch über einen minimalen Dialog auf die Parameter zugreifen und nicht über unseren Vollform-Eingabebildschirm. Daher die verschiedenen Ansichten.

Befehle, die Formen manipulieren, fallen in eine von zwei Kategorien. Entweder manipulieren sie den Schnittpfad oder sie manipulieren die Formparameter. Um die Formparameter im Allgemeinen zu manipulieren, werfen wir sie entweder zurück in den Form-Eingabebildschirm oder zeigen den minimalen Dialog an. Berechnen Sie die Form neu und zeigen Sie sie an derselben Stelle an.

Für den Schnittpfad haben wir jede Operation in einem eigenen Befehlsobjekt gebündelt. Zum Beispiel haben wir Befehlsobjekte

ResizePath RotatePath MovePath SplitPath und so weiter.

Wenn wir neue Funktionen hinzufügen müssen, fügen wir ein weiteres Befehlsobjekt hinzu, suchen ein Menü, eine Tastenkombination oder eine Symbolleistenschaltfläche im rechten UI-Bildschirm und richten das UI-Objekt ein, um diesen Befehl auszuführen.

Zum Beispiel

   CuttingTableScreen.KeyRoute.Add vbShift+vbKeyF1, New MirrorPath

oder

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

In beiden Fällen wird das Command-Objekt MirrorPath einem gewünschten UI-Element zugeordnet. In der Methode execute von MirrorPath ist der gesamte Code enthalten, der zum Spiegeln des Pfads in einer bestimmten Achse erforderlich ist. Wahrscheinlich verfügt der Befehl über ein eigenes Dialogfeld oder fragt den Benutzer mithilfe eines der Benutzeroberflächenelemente, welche Achse gespiegelt werden soll. Nichts davon macht einen Besucher oder fügt dem Pfad eine Methode hinzu.

Sie werden feststellen, dass eine Menge durch Bündelung von Aktionen in Befehlen erledigt werden kann. Ich warne jedoch davor, dass dies keine schwarze oder weiße Situation ist. Sie werden immer noch feststellen, dass bestimmte Dinge als Methoden für das ursprüngliche Objekt besser funktionieren. In der Praxis stellte sich heraus, dass vielleicht 80% meiner bisherigen Methoden in den Befehl übernommen werden konnten. Die letzten 20% arbeiten einfach besser am Objekt.

Nun mögen manche das vielleicht nicht, weil es die Verkapselung zu verletzen scheint. Ausgehend von der Pflege unserer Software als objektorientiertes System im letzten Jahrzehnt muss ich sagen, dass das Wichtigste, was Sie langfristig tun können, darin besteht, die Wechselwirkungen zwischen den verschiedenen Ebenen Ihrer Software und zwischen den verschiedenen Objekten klar zu dokumentieren.

Das Bündeln von Aktionen zu Befehlsobjekten hilft bei diesem Ziel viel besser als eine sklavische Hingabe an die Ideale der Verkapselung. Alles, was getan werden muss, um einen Pfad zu spiegeln, ist im Spiegelpfad-Befehlsobjekt gebündelt.

6
RS Conley

Das Besucherdesignmuster ist eine Problemumgehung und keine Lösung für das Problem. Kurze Antwort wäre Mustervergleich .

4
Marko Tunjic

Unabhängig davon, welchen Weg Sie einschlagen, muss die Implementierung alternativer Funktionen, die derzeit vom Besuchermuster bereitgestellt werden, etwas über die konkrete Implementierung der Schnittstelle wissen, an der sie arbeitet. Es führt also kein Weg daran vorbei, dass Sie für jede weitere Implementierung eine zusätzliche Besucherfunktion schreiben müssen. Das heißt, Sie suchen nach einem flexibleren und strukturierteren Ansatz für die Erstellung dieser Funktionalität.

Sie müssen die Besucherfunktion von der Oberfläche der Form trennen.

Was ich vorschlagen würde, ist ein kreationistischer Ansatz über eine abstrakte Fabrik, um Ersatzimplementierungen für Besucherfunktionen zu erstellen.

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

Es bedeutet immer noch, dass Sie konkrete Implementierungen schreiben müssen, um an neuen Versionen von 'shape' zu arbeiten. Da diese jedoch vollständig von der Oberfläche von shape getrennt sind, können Sie diese Lösung nachrüsten, ohne die ursprüngliche Oberfläche und die damit interagierende Software zu beschädigen. Es fungiert als eine Art Gerüst um die Implementierungen von IShape.

2
Adrian Regan

Wenn Sie Java verwenden: Ja, es heißt instanceof. Die Leute haben große Angst, es zu benutzen. Im Vergleich zum Besuchermuster ist es in der Regel schneller, unkomplizierter und nicht von Punkt 5 geplagt.

1
Andy

Wenn Sie n IShapes und m Operationen haben, die sich für jede Form unterschiedlich verhalten, benötigen Sie n * m einzelne Funktionen. Es scheint mir eine schreckliche Idee zu sein, diese alle in die gleiche Klasse zu bringen und Ihnen eine Art Gottesobjekt zu geben. Sie sollten daher entweder nach IShape gruppiert werden, indem m Funktionen, eine für jede Operation, in die IShape-Schnittstelle eingegeben werden, oder nach Operation gruppiert werden (unter Verwendung des Besuchermusters), indem n Funktionen, eine für jede IShape in jede Operation/jeden Besucher eingegeben werden Klasse.

Sie müssen entweder mehrere Klassen aktualisieren, wenn Sie eine neue IShape hinzufügen, oder wenn Sie eine neue Operation hinzufügen, führt kein Weg daran vorbei.


Wenn Sie nach jeder Operation suchen, um eine IShape-Standardfunktion zu implementieren, würde dies Ihr Problem lösen, wie in Daniel Martins Antwort: https://stackoverflow.com/a/986034/1969638 , obwohl ich wahrscheinlich verwenden würde Überlastung:

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

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

Ich habe dieses Problem tatsächlich mithilfe des folgenden Musters gelöst. Ich weiß nicht, ob es einen Namen hat oder nicht!

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

        }
    }
}

Verwendung:

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

Wenn jemand als Benutzer meiner Bibliothek IRectangleShape definiert und zeichnen möchte, kann er einfach IRectangleShapeDrawer definieren und zur Liste der Schubladen von ShapeDrawingClient hinzufügen!

0
Vahid