345 Stimmen

Wann sollte ich das Visitor Design Pattern verwenden?

Ich sehe immer wieder Hinweise auf das Besuchermuster in Blogs, aber ich muss zugeben, dass ich es nicht verstehe. Ich lese die wikipedia Artikel für das Muster und ich verstehe seine Funktionsweise, aber ich bin immer noch verwirrt, wann ich es verwenden würde.

Als jemand, der erst kürzlich wirklich hat das Dekorationsmuster und sieht nun überall Anwendungen dafür. Ich möchte dieses scheinbar praktische Muster auch wirklich intuitiv verstehen können.

19voto

davidxxx Punkte 114488

Doppelter Versand ist nur ein Grund unter anderen, dieses Muster zu verwenden .
Es ist jedoch zu beachten, dass dies der einzige Weg ist, um Double- oder More-Dispatch in Sprachen zu implementieren, die ein Single-Dispatch-Paradigma verwenden.

Hier sind Gründe, das Muster zu verwenden:

1) Wir wollen neue Operationen definieren, ohne das Modell jedes Mal zu ändern weil sich das Modell nicht oft ändert, während sich die Vorgänge häufig ändern.

2) Wir wollen Modell und Verhalten nicht koppeln denn wir wollen ein wiederverwendbares Modell haben in mehreren Anwendungen oder wir wollen ein erweiterbares Modell haben die es Client-Klassen ermöglichen, ihr Verhalten mit ihren eigenen Klassen zu definieren.

3) Wir haben gemeinsame Operationen, die vom konkreten Typ des Modells abhängen, aber wir wollen die Logik nicht in jeder Unterklasse implementieren, da dies die gemeinsame Logik in mehreren Klassen und somit an mehreren Stellen auflösen würde .

4) Wir verwenden einen Domänenmodellentwurf und Modellklassen derselben Hierarchie führen zu viele verschiedene Aufgaben aus, die an anderer Stelle zusammengefasst werden könnten .

5) Wir brauchen einen doppelten Versand .
Wir haben Variablen, die mit Schnittstellentypen deklariert sind, und wir wollen sie entsprechend ihres Laufzeittyps verarbeiten können natürlich ohne die Verwendung von if (myObj instanceof Foo) {} oder irgendeinen Trick.
Die Idee ist zum Beispiel, diese Variablen an Methoden zu übergeben, die einen konkreten Typ der Schnittstelle als Parameter deklarieren, um eine bestimmte Verarbeitung anzuwenden. Diese Vorgehensweise ist bei Sprachen, die auf einem Single-Dispatch beruhen, nicht ohne weiteres möglich, da der gewählte Aufruf zur Laufzeit nur vom Laufzeittyp des Empfängers abhängt.
Beachten Sie, dass in Java die aufzurufende Methode (Signatur) zur Kompilierzeit ausgewählt wird und vom deklarierten Typ der Parameter abhängt, nicht von ihrem Laufzeittyp.

Der letzte Punkt, der ein Grund für die Verwendung des Besuchers ist, ist auch eine Konsequenz, denn wenn Sie den Besucher implementieren (natürlich für Sprachen, die keine Mehrfachabfertigung unterstützen), müssen Sie zwangsläufig eine Implementierung für die Doppelabfertigung einführen.

Beachten Sie, dass das Durchlaufen der Elemente (Iteration), um den Besucher auf jedes einzelne anzuwenden, kein Grund ist, das Muster zu verwenden.
Sie verwenden das Muster, weil Sie Modell und Verarbeitung trennen.
Und durch die Verwendung des Musters profitieren Sie zusätzlich von einer Iterator-Fähigkeit.
Diese Fähigkeit ist sehr mächtig und geht über die Iteration eines allgemeinen Typs mit einer bestimmten Methode wie accept() ist eine generische Methode.
Dies ist ein spezieller Anwendungsfall. Ich werde das also beiseite lassen.


Beispiel in Java

Ich werde den Mehrwert des Musters anhand eines Schachbeispiels veranschaulichen, bei dem wir die Verarbeitung definieren möchten, wenn ein Spieler eine Figur bewegen möchte.

Ohne die Verwendung des Besuchermusters könnten wir das Verhalten beim Verschieben von Teilen direkt in den Unterklassen der Teile definieren.
Wir könnten zum Beispiel eine Piece Schnittstelle wie z. B. :

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Jede Unterklasse von Piece würde sie implementieren, wie z.B. :

public class Pawn implements Piece{

    @Override
    public boolean checkMoveValidity(Coordinates coord) {
        ...
    }

    @Override
    public void performMove(Coordinates coord) {
        ...
    }

    @Override
    public Piece computeIfKingCheck() {
        ...
    }

}

Und das Gleiche gilt für alle Unterklassen von Piece.
Das folgende Diagramm veranschaulicht diesen Entwurf:

model class diagram

Dieser Ansatz hat drei wesentliche Nachteile:

- Verhaltensweisen wie performMove() o computeIfKingCheck() wird höchstwahrscheinlich die allgemeine Logik anwenden.
Zum Beispiel, was auch immer der konkrete Piece , performMove() setzt schließlich die aktuelle Figur an einen bestimmten Ort und nimmt möglicherweise die gegnerische Figur.
Die Aufteilung verwandter Verhaltensweisen in mehrere Klassen, anstatt sie zusammenzufassen, untergräbt in gewisser Weise das Muster der einzigen Verantwortung. Das macht ihre Wartbarkeit schwieriger.

- Verarbeitung als checkMoveValidity() sollte nicht etwas sein, das die Piece Unterklassen sehen oder ändern können.
Es handelt sich um eine Kontrolle, die über das Handeln von Menschen oder Computern hinausgeht. Diese Prüfung wird bei jeder von einem Spieler angeforderten Aktion durchgeführt, um sicherzustellen, dass der angeforderte Figurenzug gültig ist.
Wir wollen also nicht einmal das in der Piece Schnittstelle.

- Bei Schachspielen, die für Bot-Entwickler eine Herausforderung darstellen, bietet die Anwendung im Allgemeinen eine Standard-API ( Piece Schnittstellen, Unterklassen, Board, gemeinsame Verhaltensweisen usw.) und lassen Entwickler ihre Bot-Strategie bereichern.
Um dies zu erreichen, müssen wir ein Modell vorschlagen, bei dem Daten und Verhaltensweisen nicht eng miteinander gekoppelt sind. Piece Implementierungen.

Verwenden wir also das Besuchermuster!

Wir haben zwei Arten von Strukturen:

- die Modellklassen, die besucht werden dürfen (die Stücke)

- die Besucher, die sie aufsuchen (bewegliche Vorgänge)

Hier ist ein Klassendiagramm, das das Muster veranschaulicht:

enter image description here

Im oberen Teil haben wir die Besucher und im unteren Teil die Modellklassen.

Hier ist die PieceMovingVisitor Schnittstelle (das Verhalten ist für jede Art von Piece ) :

public interface PieceMovingVisitor {

    void visitPawn(Pawn pawn);

    void visitKing(King king);

    void visitQueen(Queen queen);

    void visitKnight(Knight knight);

    void visitRook(Rook rook);

    void visitBishop(Bishop bishop);

}

Das Stück ist jetzt definiert:

public interface Piece {

    void accept(PieceMovingVisitor pieceVisitor);

    Coordinates getCoordinates();

    void setCoordinates(Coordinates coordinates);

}

Seine wichtigste Methode ist :

void accept(PieceMovingVisitor pieceVisitor);

Es bietet den ersten Versand: einen Aufruf auf der Grundlage der Piece Empfänger.
Zur Kompilierungszeit ist die Methode an die accept() Methode der Schnittstelle Piece und zur Laufzeit wird die Methode Bounded auf der Laufzeit Piece Klasse.
Und es ist die accept() Methodenimplementierung, die einen zweiten Versand durchführen wird.

In der Tat, jeder Piece Unterklasse, die besucht werden möchte von einer PieceMovingVisitor Objekt ruft die PieceMovingVisitor.visit() Methode, indem sie selbst als Argument übergeben wird.
Auf diese Weise grenzt der Compiler bereits zur Kompilierzeit den Typ des deklarierten Parameters mit dem konkreten Typ ab.
Es gibt die zweite Sendung.
Hier ist die Bishop Unterklasse, die veranschaulicht, dass :

public class Bishop implements Piece {

    private Coordinates coord;

    public Bishop(Coordinates coord) {
        super(coord);
    }

    @Override
    public void accept(PieceMovingVisitor pieceVisitor) {
        pieceVisitor.visitBishop(this);
    }

    @Override
    public Coordinates getCoordinates() {
        return coordinates;
    }

   @Override
    public void setCoordinates(Coordinates coordinates) {
        this.coordinates = coordinates;
   }

}

Und hier ein Anwendungsbeispiel:

// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();

// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);

// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
    piece.accept(new MovePerformingVisitor(coord));
}

Nachteile für Besucher

Das Visitor-Muster ist ein sehr leistungsfähiges Muster, hat aber auch einige wichtige Einschränkungen, die Sie berücksichtigen sollten, bevor Sie es verwenden.

1) Risiko der Verringerung/Brechung der Verkapselung

Bei einigen Betriebsarten kann das Besuchermuster die Kapselung von Domänenobjekten verringern oder aufheben.

Zum Beispiel, als die MovePerformingVisitor Klasse muss die Koordinaten des eigentlichen Stücks festlegen, die Piece Schnittstelle muss eine Möglichkeit bieten, dies zu tun:

void setCoordinates(Coordinates coordinates);

Die Verantwortung der Piece Koordinatenänderungen sind nun auch für andere Klassen als Piece Unterklassen.
Verschieben der Verarbeitung durch den Besucher im Piece Unterklassen ist ebenfalls keine Option.
Dies wird in der Tat ein weiteres Problem darstellen, da die Piece.accept() akzeptiert jede Besucherimplementierung. Es weiß nicht, was der Besucher tut und hat daher keine Ahnung, ob und wie es den Zustand des Stücks ändern kann.
Eine Möglichkeit, den Besucher zu identifizieren, wäre eine Nachbearbeitung in Piece.accept() je nach der Implementierung des Besuchers. Es wäre eine sehr schlechte Idee, da es eine hohe Kopplung zwischen Visitor-Implementierungen und Piece-Unterklassen schaffen würde und außerdem würde es wahrscheinlich erfordern, Trick als getClass() , instanceof oder eine beliebige Markierung, die die Implementierung des Besuchers kennzeichnet.

2) Erfordernis, das Modell zu ändern

Im Gegensatz zu einigen anderen verhaltensorientierten Entwurfsmustern wie Decorator zum Beispiel ist das Besuchermuster aufdringlich.
Wir müssen in der Tat die ursprüngliche Empfängerklasse ändern, um eine accept() Methode zu akzeptieren, um besucht zu werden.
Wir hatten keine Probleme mit der Piece und ihre Unterklassen, da diese unsere Klassen .
Bei integrierten Klassen oder Klassen von Drittanbietern sind die Dinge nicht so einfach.
Wir müssen sie einpacken oder vererben (wenn wir können), um die accept() Methode.

3) Indirektionen

Das Muster erzeugt mehrfache Umwege.
Die doppelte Versendung bedeutet zwei Aufrufe anstelle eines einzigen Aufrufs:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor)

Und wir könnten zusätzliche Umleitungen haben, wenn der Besucher den Zustand des besuchten Objekts ändert.
Es mag wie ein Zyklus aussehen:

call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)

14voto

Richard Gomes Punkte 5140

Es gibt mindestens drei sehr gute Gründe für die Verwendung des Besuchermusters:

  1. Verringerung der Verbreitung von Code, der sich nur geringfügig ändert, wenn sich Datenstrukturen ändern.

  2. Die gleiche Berechnung auf mehrere Datenstrukturen anwenden, ohne den Code zu ändern, der die Berechnung implementiert.

  3. Hinzufügen von Informationen zu Legacy-Bibliotheken ohne Änderung des Legacy-Codes.

Schauen Sie sich bitte an einen Artikel, den ich darüber geschrieben habe .

13voto

Kapoor Punkte 1340

Wie Konrad Rudolph bereits dargelegt hat, ist sie für Fälle geeignet, in denen wir Doppelversand

Das folgende Beispiel zeigt eine Situation, in der wir einen doppelten Versand benötigen und wie der Besucher uns dabei hilft.

Beispiel:

Nehmen wir an, ich habe 3 Arten von mobilen Geräten - iPhone, Android, Windows Mobile.

Alle drei Geräte sind mit einem Bluetooth-Funkgerät ausgestattet.

Gehen wir davon aus, dass das Blauzahnradio von 2 verschiedenen OEMs stammen kann - Intel und Broadcom.

Um das Beispiel für unsere Diskussion relevant zu machen, nehmen wir an, dass sich die APIs, die von Intel-Funkgeräten bereitgestellt werden, von denen unterscheiden, die von Broadcom-Funkgeräten bereitgestellt werden.

So sieht mein Unterricht aus -

enter image description here enter image description here

Jetzt möchte ich einen Vorgang vorstellen - das Einschalten von Bluetooth auf einem mobilen Gerät.

Seine Funktionssignatur sollte etwa so aussehen -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

Also je nach Der richtige Gerätetyp y Abhängig vom richtigen Typ des Bluetooth-Funkgeräts kann es eingeschaltet werden durch Aufruf der entsprechenden Schritte oder des Algorithmus .

Im Prinzip handelt es sich um eine 3 x 2-Matrix, bei der ich versuche, die richtige Operation in Abhängigkeit von der richtigen Art der beteiligten Objekte zu vektorisieren.

Ein polymorphes Verhalten in Abhängigkeit vom Typ der beiden Argumente.

enter image description here

Nun kann das Besuchermuster auf dieses Problem angewendet werden. Die Inspiration kommt von der Wikipedia-Seite, die besagt - "Im Wesentlichen ermöglicht der Visitor das Hinzufügen neuer virtueller Funktionen zu einer Familie von Klassen, ohne die Klassen selbst zu verändern; stattdessen erstellt man eine Visitor-Klasse, die alle entsprechenden Spezialisierungen der virtuellen Funktion implementiert. Der Visitor nimmt die Instanzreferenz als Eingabe und implementiert das Ziel durch Double Dispatch."

Aufgrund der 3x2-Matrix ist hier ein doppelter Versand notwendig

So wird der Aufbau aussehen - enter image description here

Ich habe das Beispiel geschrieben, um eine andere Frage zu beantworten, der Code und seine Erklärung sind erwähnt aquí .

9voto

Ich fand es einfacher, den Links zu folgen:

Unter http://www.remondo.net/visitor-pattern-example-csharp/ Ich habe ein Beispiel gefunden, das ein Beispiel zeigt, das zeigt, was der Nutzen des Besuchermusters ist. Hier haben Sie verschiedene Container-Klassen für Pill :

namespace DesignPatterns
{
    public class BlisterPack
    {
        // Pairs so x2
        public int TabletPairs { get; set; }
    }

    public class Bottle
    {
        // Unsigned
        public uint Items { get; set; }
    }

    public class Jar
    {
        // Signed
        public int Pieces { get; set; }
    }
}

Wie Sie oben sehen, können Sie BilsterPack Paare von Pillen' enthalten, so dass Sie die Anzahl der Paare mit 2 multiplizieren müssen. Außerdem werden Sie feststellen, dass Bottle verwenden. unit die einen anderen Datentyp haben und gecastet werden müssen.

In der Hauptmethode können Sie also die Anzahl der Pillen mit folgendem Code berechnen:

foreach (var item in packageList)
{
    if (item.GetType() == typeof (BlisterPack))
    {
        pillCount += ((BlisterPack) item).TabletPairs * 2;
    }
    else if (item.GetType() == typeof (Bottle))
    {
        pillCount += (int) ((Bottle) item).Items;
    }
    else if (item.GetType() == typeof (Jar))
    {
        pillCount += ((Jar) item).Pieces;
    }
}

Beachten Sie, dass der obige Code gegen die Single Responsibility Principle . Das bedeutet, dass Sie den Code der Hauptmethode ändern müssen, wenn Sie einen neuen Containertyp hinzufügen. Außerdem ist es schlechte Praxis, den Schalter länger zu machen.

Also, indem Sie folgenden Code einführen:

public class PillCountVisitor : IVisitor
{
    public int Count { get; private set; }

    #region IVisitor Members

    public void Visit(BlisterPack blisterPack)
    {
        Count += blisterPack.TabletPairs * 2;
    }

    public void Visit(Bottle bottle)
    {
        Count += (int)bottle.Items;
    }

    public void Visit(Jar jar)
    {
        Count += jar.Pieces;
    }

    #endregion
}

Sie haben die Verantwortung für das Zählen der Anzahl von Pill s zur Klasse namens PillCountVisitor (Und wir haben die Anweisung switch case entfernt). Das bedeutet, dass Sie, wann immer Sie einen neuen Typ von Pillenbehälter hinzufügen müssen, nur die PillCountVisitor Klasse. Beachten Sie auch IVisitor Schnittstelle ist allgemein für die Verwendung in anderen Szenarien.

Durch Hinzufügen der Accept-Methode zur Pill Container-Klasse:

public class BlisterPack : IAcceptor
{
    public int TabletPairs { get; set; }

    #region IAcceptor Members

    public void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }

    #endregion
}

erlauben wir dem Besucher den Besuch von Pillendosen-Klassen.

Am Ende berechnen wir die Anzahl der Pillen mit folgendem Code:

var visitor = new PillCountVisitor();

foreach (IAcceptor item in packageList)
{
    item.Accept(visitor);
}

Das bedeutet: Jeder Pillenbehälter erlaubt die PillCountVisitor Besucher, die ihre Pillen zählen lassen. Er weiß, wie Sie Ihre Pillen zählen können.

In der visitor.Count hat den Wert von Pillen.

Unter http://butunclebob.com/ArticleS.UncleBob.IuseVisitor Sie sehen ein reales Szenario, in dem Sie nicht mit Polymorphismus (die Antwort) dem Grundsatz der Einzelverantwortung zu folgen. In der Tat in:

public class HourlyEmployee extends Employee {
  public String reportQtdHoursAndPay() {
    //generate the line for this hourly employee
  }
}

die reportQtdHoursAndPay Diese Methode dient der Berichterstattung und Vertretung und verstößt gegen den Grundsatz der einzigen Verantwortung. Daher ist es besser, das Problem mit Hilfe des Besuchermusters zu lösen.

7voto

andrew pate Punkte 3251

Kurze Beschreibung des Besuchermusters. Die Klassen, die geändert werden müssen, müssen alle die Methode "accept" implementieren. Die Clients rufen diese Accept-Methode auf, um eine neue Aktion für diese Klassenfamilie durchzuführen und damit deren Funktionalität zu erweitern. Clients können diese eine Accept-Methode verwenden, um eine breite Palette neuer Aktionen durchzuführen, indem sie für jede spezifische Aktion eine andere Besucherklasse übergeben. Eine Besucherklasse enthält mehrere überschriebene Besuchermethoden, die festlegen, wie dieselbe spezifische Aktion für jede Klasse innerhalb der Familie erreicht werden soll. Diesen Besuchsmethoden wird eine Instanz übergeben, mit der sie arbeiten können.

Wann Sie es verwenden sollten

  1. Wenn Sie eine Familie von Klassen haben, wissen Sie, dass Sie viele neue Aktionen hinzufügen müssen, aber aus irgendeinem Grund sind Sie nicht in der Lage, die Familie von Klassen in Zukunft zu ändern oder neu zu kompilieren.
  2. Wenn Sie eine neue Aktion hinzufügen möchten und diese neue Aktion vollständig in einer Besucherklasse definiert werden soll, anstatt sie auf mehrere Klassen zu verteilen.
  3. Wenn Ihr Chef sagt, dass Sie eine Reihe von Klassen produzieren müssen, die etwas leisten müssen im Augenblick !... aber niemand weiß bisher genau, was dieses Etwas ist.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X