Wann sollte ich das Besucherdesignmuster verwenden? [geschlossen]

315

Ich sehe immer wieder Verweise auf das Besuchermuster in Blogs, aber ich muss zugeben, ich verstehe es einfach nicht. Ich habe den Wikipedia-Artikel für das Muster gelesen und verstehe seine Mechanik, bin aber immer noch verwirrt, wann ich es verwenden würde.

Als jemand, der erst kürzlich das Dekorationsmuster wirklich bekommen hat und jetzt absolut überall Verwendung dafür sieht, möchte ich dieses scheinbar handliche Muster auch wirklich intuitiv verstehen können.

George Mauer
quelle
7
Endlich, nachdem ich diesen Artikel von Jermey Miller über meine Brombeere gelesen hatte, während ich zwei Stunden in einer Lobby wartete. Es ist lang, bietet aber eine wunderbare Erklärung für Doppelversand, Besucher und Verbundwerkstoff und was Sie damit machen können.
George Mauer
1
Hier ist ein schöner Artikel: codeproject.com/Articles/186185/Visitor-Design-Pattern
Seyed Morteza Mousavi
3
Besuchermuster? Welcher? Der Punkt ist: Es gibt viele Missverständnisse und reine Verwirrung um dieses Designmuster. Ich habe einen Artikel geschrieben, der diesem Chaos hoffentlich Ordnung verleiht
Richard Gomes
Wenn Sie Funktionsobjekte für Union-Datentypen haben möchten, benötigen Sie ein Besuchermuster. Sie fragen sich vielleicht, was Funktionsobjekte und Union-Datentypen sind, dann lohnt es sich, ccs.neu.edu/home/matthias/htdc.html
Wei Qiu
Beispiele hier und hier .
jaco0646

Antworten:

315

Ich bin mit dem Besuchermuster nicht sehr vertraut. Mal sehen, ob ich es richtig verstanden habe. Angenommen, Sie haben eine Hierarchie von Tieren

class Animal {  };
class Dog: public Animal {  };
class Cat: public Animal {  };

(Angenommen, es handelt sich um eine komplexe Hierarchie mit einer gut etablierten Schnittstelle.)

Jetzt wollen wir der Hierarchie eine neue Operation hinzufügen, nämlich, dass jedes Tier seinen Klang erzeugt. Soweit die Hierarchie so einfach ist, können Sie dies mit geradlinigem Polymorphismus tun:

class Animal
{ public: virtual void makeSound() = 0; };

class Dog : public Animal
{ public: void makeSound(); };

void Dog::makeSound()
{ std::cout << "woof!\n"; }

class Cat : public Animal
{ public: void makeSound(); };

void Cat::makeSound()
{ std::cout << "meow!\n"; }

Wenn Sie jedoch auf diese Weise vorgehen, müssen Sie jedes Mal, wenn Sie eine Operation hinzufügen möchten, die Schnittstelle für jede einzelne Klasse der Hierarchie ändern. Nehmen wir stattdessen an, dass Sie mit der ursprünglichen Benutzeroberfläche zufrieden sind und möglichst wenige Änderungen daran vornehmen möchten.

Mit dem Besuchermuster können Sie jede neue Operation in eine geeignete Klasse verschieben und die Schnittstelle der Hierarchie nur einmal erweitern. Machen wir das. Zunächst definieren wir eine abstrakte Operation (die "Visitor" -Klasse in GoF ), die für jede Klasse in der Hierarchie eine Methode enthält:

class Operation
{
public:
    virtual void hereIsADog(Dog *d) = 0;
    virtual void hereIsACat(Cat *c) = 0;
};

Dann ändern wir die Hierarchie, um neue Operationen zu akzeptieren:

class Animal
{ public: virtual void letsDo(Operation *v) = 0; };

class Dog : public Animal
{ public: void letsDo(Operation *v); };

void Dog::letsDo(Operation *v)
{ v->hereIsADog(this); }

class Cat : public Animal
{ public: void letsDo(Operation *v); };

void Cat::letsDo(Operation *v)
{ v->hereIsACat(this); }

Schließlich implementieren wir die eigentliche Operation, ohne weder Katze noch Hund zu ändern :

class Sound : public Operation
{
public:
    void hereIsADog(Dog *d);
    void hereIsACat(Cat *c);
};

void Sound::hereIsADog(Dog *d)
{ std::cout << "woof!\n"; }

void Sound::hereIsACat(Cat *c)
{ std::cout << "meow!\n"; }

Jetzt haben Sie die Möglichkeit, Operationen hinzuzufügen, ohne die Hierarchie mehr zu ändern. So funktioniert es:

int main()
{
    Cat c;
    Sound theSound;
    c.letsDo(&theSound);
}
Federico A. Ramponi
quelle
19
S.Lott, auf einem Baum spazieren zu gehen, ist eigentlich nicht das Besuchermuster. (Es ist das "hierarchische Besuchermuster", das verwirrenderweise völlig anders ist.) Es gibt keine Möglichkeit, das GoF-Besuchermuster anzuzeigen, ohne Vererbung oder Schnittstellenimplementierung zu verwenden.
Munificent
14
@Knownasilya - Das ist nicht wahr. Der & -Operator gibt die Adresse des Sound-Objekts an, die von der Schnittstelle benötigt wird. letsDo(Operation *v) braucht einen Zeiger.
AquilaRapax
3
Ist dieses Beispiel für ein Besucher-Designmuster aus Gründen der Klarheit korrekt?
Godzilla
4
Nach langem Überlegen frage ich mich, warum Sie hier zwei Methoden isIsADog und hereIsACat genannt haben, obwohl Sie den Hund und die Katze bereits an die Methoden übergeben haben. Ich würde eine einfache performTask (Object * obj) bevorzugen und Sie wandeln dieses Objekt in die Operation-Klasse. (und in der Sprache, die das Überschreiben unterstützt, ist kein Casting erforderlich)
Abdalrahman Shatou
6
In Ihrem "Haupt" -Beispiel am Ende: theSound.hereIsACat(c)Hätte die Arbeit erledigt, wie rechtfertigen Sie den gesamten durch das Muster verursachten Overhead? Doppelversand ist die Rechtfertigung.
Franssu
131

Der Grund für Ihre Verwirrung ist wahrscheinlich, dass der Besucher eine fatale Fehlbezeichnung ist. Viele (prominente 1 !) Programmierer sind über dieses Problem gestolpert. Tatsächlich wird Double Dispatching in Sprachen implementiert , die es nicht nativ unterstützen (die meisten von ihnen nicht).


1) Mein Lieblingsbeispiel ist Scott Meyers, gefeierter Autor von „Effective C ++“, der dies als eines seiner wichtigsten C ++ Aha bezeichnet! Momente überhaupt .

Konrad Rudolph
quelle
3
+1 "Es gibt kein Muster" - die perfekte Antwort. Die am besten bewertete Antwort beweist, dass viele C ++ - Programmierer die Einschränkungen virtueller Funktionen gegenüber dem "Ad-hoc" -Polymorphismus noch nicht erkannt haben, indem sie eine Aufzählung vom Typ enum und switch (c-way) verwenden. Es mag ordentlicher und unsichtbarer sein, virtuell zu verwenden, aber es ist immer noch auf den Einzelversand beschränkt. Meiner persönlichen Meinung nach ist dies der größte Fehler von c ++.
user3125280
@ user3125280 Ich habe jetzt 4/5 Artikel und das Kapitel "Entwurfsmuster" über das Besuchermuster gelesen, und keiner von ihnen erklärt den Vorteil der Verwendung dieses obskuren Musters gegenüber einem Fall-Stmt oder wenn Sie eines über dem anderen verwenden könnten. Danke, dass du es zumindest angesprochen hast!
Spinkus
4
@ Sam Ich bin mir ziemlich sicher , dass sie es erklären - es ist der gleiche Vorteil , dass Sie immer bekommen von Subklassen / Laufzeit - Polymorphismus über switch: switchhart-Codes der Entscheidung auf der Clientseite zu machen (Code - Duplizierung) und bietet keine statische Typprüfung ( auf Vollständigkeit und Unterscheidbarkeit der Fälle usw. prüfen. Ein Besuchermuster wird von der Typprüfung überprüft und vereinfacht normalerweise den Clientcode.
Konrad Rudolph
@KonradRudolph danke dafür. Beachten Sie jedoch, dass dies beispielsweise in Patterns oder im Wikipedia-Artikel nicht explizit angesprochen wird. Ich bin nicht anderer Meinung als Sie, aber Sie könnten argumentieren, dass die Verwendung eines case stmt auch Vorteile hat, so dass es seltsam ist, dass es nicht generell kontrastiert: 1. Sie benötigen keine accept () -Methode für Objekte Ihrer Sammlung. 2. Der Besucher kann Objekte unbekannten Typs verarbeiten. Daher scheint der Fall stmt besser für die Bearbeitung von Objektstrukturen mit einer veränderbaren Sammlung von beteiligten Typen geeignet zu sein. Patterns räumt ein, dass das Besuchermuster für ein solches Szenario nicht gut geeignet ist (S. 333).
spinkus
1
@SamPinkus konrad ist genau richtig - deshalb sind virtualähnliche Funktionen in modernen Programmiersprachen so nützlich - sie sind der Grundbaustein erweiterbarer Programme - meiner Meinung nach ist der c-Weg (verschachtelter Schalter oder Musterübereinstimmung usw., abhängig von der Sprache Ihrer Wahl) weitaus sauberer in Code, der nicht erweiterbar sein muss, und ich war angenehm überrascht, diesen Stil in komplizierter Software wie Prover 9 zu sehen. Noch wichtiger ist, dass jede Sprache, die Erweiterbarkeit bieten möchte, wahrscheinlich bessere Versandmuster berücksichtigen sollte als rekursiver Einzelversand (dh Besucher).
user3125280
84

Jeder hier hat Recht, aber ich denke, es wird das "Wann" nicht angesprochen. Zunächst aus Design Patterns:

Mit Visitor können Sie eine neue Operation definieren, ohne die Klassen der Elemente zu ändern, für die sie ausgeführt wird.

Stellen wir uns nun eine einfache Klassenhierarchie vor. Ich habe die Klassen 1, 2, 3 und 4 und die Methoden A, B, C und D. Legen Sie sie wie in einer Tabelle an: Die Klassen sind Zeilen und die Methoden sind Spalten.

Jetzt geht das objektorientierte Design davon aus, dass Sie mit größerer Wahrscheinlichkeit neue Klassen als neue Methoden erweitern, sodass das Hinzufügen von mehr Zeilen sozusagen einfacher ist. Sie fügen einfach eine neue Klasse hinzu, geben an, was in dieser Klasse anders ist, und erben den Rest.

Manchmal sind die Klassen zwar relativ statisch, aber Sie müssen häufig mehr Methoden hinzufügen - Spalten hinzufügen. Die Standardmethode in einem OO-Entwurf besteht darin, allen Klassen solche Methoden hinzuzufügen, was kostspielig sein kann. Das Besuchermuster macht dies einfach.

Dies ist übrigens das Problem, das Scalas Musterübereinstimmungen lösen sollen.

Daniel C. Sobral
quelle
Warum sollte ich das Besuchermuster nur für eine Utlity-Klasse verwenden? Ich kann meine Utility-Klasse folgendermaßen aufrufen: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Was ist der Unterschied ? beide trennen sich, oder? hoffe du kannst helfen
j2emanue
@ j2emanue Da das Besuchermuster zur Laufzeit die korrekte Überlastung des Besuchers verwendet. Während Ihr Code Typumwandlung benötigt, um die korrekte Überladung aufzurufen.
Zugriff verweigert
Gibt es damit einen Effizienzgewinn? Ich denke, es vermeidet es, eine gute Idee zu machen
j2emanue
@ j2emanue Die Idee ist, Code zu schreiben, der dem Open / Closed-Prinzip entspricht, nicht aus Leistungsgründen. Siehe offen geschlossen bei Onkel Bob butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod
Zugriff verweigert
22

Das Besucherentwurfsmuster eignet sich sehr gut für "rekursive" Strukturen wie Verzeichnisbäume, XML-Strukturen oder Dokumentumrisse.

Ein Besucherobjekt besucht jeden Knoten in der rekursiven Struktur: jedes Verzeichnis, jedes XML-Tag, was auch immer. Das Visitor-Objekt durchläuft die Struktur nicht. Stattdessen werden Besuchermethoden auf jeden Knoten der Struktur angewendet.

Hier ist eine typische rekursive Knotenstruktur. Könnte ein Verzeichnis oder ein XML-Tag sein. [Wenn Sie eine Java-Person sind, stellen Sie sich viele zusätzliche Methoden zum Erstellen und Verwalten der Kinderliste vor.]

class TreeNode( object ):
    def __init__( self, name, *children ):
        self.name= name
        self.children= children
    def visit( self, someVisitor ):
        someVisitor.arrivedAt( self )
        someVisitor.down()
        for c in self.children:
            c.visit( someVisitor )
        someVisitor.up()

Die visitMethode wendet auf jeden Knoten in der Struktur ein Besucherobjekt an. In diesem Fall ist es ein Top-Down-Besucher. Sie können die Struktur der visitMethode ändern , um Bottom-up oder eine andere Reihenfolge zu erreichen.

Hier ist eine Superklasse für Besucher. Es wird von der visitMethode verwendet. Es "erreicht" jeden Knoten in der Struktur. Da die visitMethode upund aufruft down, kann der Besucher die Tiefe verfolgen.

class Visitor( object ):
    def __init__( self ):
        self.depth= 0
    def down( self ):
        self.depth += 1
    def up( self ):
        self.depth -= 1
    def arrivedAt( self, aTreeNode ):
        print self.depth, aTreeNode.name

Eine Unterklasse kann beispielsweise Knoten auf jeder Ebene zählen und eine Liste von Knoten akkumulieren, wodurch eine schöne Pfadhierarchie für hierarchische Abschnitte generiert wird.

Hier ist eine Anwendung. Es baut eine Baumstruktur auf someTree. Es schafft eine Visitor, dumpNodes.

Dann gilt das dumpNodesfür den Baum. Das dumpNodeObjekt "besucht" jeden Knoten im Baum.

someTree= TreeNode( "Top", TreeNode("c1"), TreeNode("c2"), TreeNode("c3") )
dumpNodes= Visitor()
someTree.visit( dumpNodes )

Der TreeNode- visitAlgorithmus stellt sicher , dass jeder TreeNode als Argument für die Besuchermethode arrivedAtverwendet wird.

S.Lott
quelle
8
Wie andere angegeben haben, ist dies das "hierarchische Besuchermuster".
PPC-Coder
1
@ PPC-Coder Was ist der Unterschied zwischen 'hierarchischem Besuchermuster' und Besuchermuster?
Tim Lovell-Smith
3
Das hierarchische Besuchermuster ist flexibler als das klassische Besuchermuster. Mit dem hierarchischen Muster können Sie beispielsweise die Tiefe der Durchquerung verfolgen und entscheiden, welcher Zweig durchlaufen werden soll, oder die Durchquerung insgesamt beenden. Der klassische Besucher hat dieses Konzept nicht und besucht alle Knoten.
PPC-Coder
18

Eine Möglichkeit, dies zu betrachten, besteht darin, dass das Besuchermuster es Ihren Kunden ermöglicht, allen Klassen in einer bestimmten Klassenhierarchie zusätzliche Methoden hinzuzufügen.

Dies ist nützlich, wenn Sie eine ziemlich stabile Klassenhierarchie haben, sich jedoch die Anforderungen ändern, was mit dieser Hierarchie zu tun ist.

Das klassische Beispiel ist für Compiler und dergleichen. Ein abstrakter Syntaxbaum (AST) kann die Struktur der Programmiersprache genau definieren, aber die Operationen, die Sie möglicherweise mit dem AST ausführen möchten, ändern sich im Verlauf Ihres Projekts: Codegeneratoren, hübsche Drucker, Debugger, Analyse von Komplexitätsmetriken.

Ohne das Besuchermuster musste ein Entwickler jedes Mal, wenn er ein neues Feature hinzufügen wollte, diese Methode zu jedem Feature in der Basisklasse hinzufügen. Dies ist besonders schwierig, wenn die Basisklassen in einer separaten Bibliothek angezeigt werden oder von einem separaten Team erstellt werden.

(Ich habe gehört, dass argumentiert wurde, dass das Besuchermuster im Widerspruch zu guten OO-Praktiken steht, weil es die Operationen der Daten von den Daten wegbewegt. Das Besuchermuster ist genau in der Situation nützlich, in der die normalen OO-Praktiken fehlschlagen.)

Seltsames Denken
quelle
Ich möchte auch Ihre Meinung zu Folgendem: Warum sollte ich das Besuchermuster nur für eine Utlity-Klasse verwenden? Ich kann meine Utility-Klasse folgendermaßen aufrufen: AnalyticsManger.visit (someObjectToVisit) vs AnalyticsVisitor.visit (someOjbectToVisit). Was ist der Unterschied ? beide trennen sich, oder? hoffe du kannst helfen
j2emanue
@ j2emanue: Ich verstehe die Frage nicht. Ich schlage vor, Sie konkretisieren es und stellen es als vollständige Frage, die jeder beantworten kann.
Oddthinking
1
Ich habe hier eine neue Frage gestellt: stackoverflow.com/questions/52068876/…
j2emanue
14

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

  1. Reduzieren Sie die Verbreitung von Code, die sich nur geringfügig unterscheidet, wenn sich Datenstrukturen ändern.

  2. Wenden Sie dieselbe Berechnung auf mehrere Datenstrukturen an, ohne den Code zu ändern, der die Berechnung implementiert.

  3. Fügen Sie Informationen zu Legacy-Bibliotheken hinzu, ohne den Legacy-Code zu ändern.

Bitte schauen Sie sich einen Artikel an, den ich darüber geschrieben habe .

Richard Gomes
quelle
1
Ich habe Ihren Artikel mit der größten Verwendung kommentiert, die ich für Besucher gesehen habe. Gedanken?
George Mauer
13

Wie Konrad Rudolph bereits betont hat, ist es für Fälle geeignet, in denen wir einen doppelten Versand benötigen

Hier ist ein Beispiel, um eine Situation zu zeigen, in der wir einen doppelten Versand benötigen und wie der Besucher uns dabei hilft.

Beispiel:

Nehmen wir an, ich habe drei Arten von Mobilgeräten - iPhone, Android, Windows Mobile.

Auf allen diesen drei Geräten ist ein Bluetooth-Radio installiert.

Nehmen wir an, dass das Bluetooth-Radio von zwei verschiedenen OEMs stammen kann - Intel & Broadcom.

Um das Beispiel für unsere Diskussion relevant zu machen, nehmen wir auch an, dass sich die von Intel Radio bereitgestellten APIs von denen von Broadcom Radio unterscheiden.

So sehen meine Klassen aus -

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Jetzt möchte ich eine Operation vorstellen - Bluetooth auf einem mobilen Gerät einschalten.

Die Funktionssignatur sollte ungefähr so ​​aussehen -

 void SwitchOnBlueTooth(IMobileDevice mobileDevice, IBlueToothRadio blueToothRadio)

Abhängig vom richtigen Gerätetyp und vom richtigen Bluetooth-Funktyp kann es durch Aufrufen geeigneter Schritte oder Algorithmen eingeschaltet werden .

Im Prinzip wird es eine 3 x 2-Matrix, in der ich versuche, die richtige Operation abhängig von der richtigen Art der beteiligten Objekte zu vektorisieren.

Ein polymorphes Verhalten, das von der Art der beiden Argumente abhängt.

Geben Sie hier die Bildbeschreibung ein

Jetzt kann das Besuchermuster auf dieses Problem angewendet werden. Die Inspiration stammt von der Wikipedia-Seite, auf der es heißt: „Im Wesentlichen erlaubt der Besucher, einer Klassenfamilie neue virtuelle Funktionen hinzuzufügen, ohne die Klassen selbst zu ändern. Stattdessen wird eine Besucherklasse erstellt, die alle entsprechenden Spezialisierungen der virtuellen Funktion implementiert. Der Besucher nimmt die Instanzreferenz als Eingabe und implementiert das Ziel durch doppelten Versand. “

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

So wird das Setup aussehen - Geben Sie hier die Bildbeschreibung ein

Ich schrieb das Beispiel eine andere Frage zu beantworten, der Code seine Erklärung erwähnt hier .

Kapoor
quelle
9

Ich fand es einfacher in folgenden Links:

In http://www.remondo.net/visitor-pattern-example-csharp/ habe ich ein Beispiel gefunden, das ein Scheinbeispiel zeigt, das den Nutzen des Besuchermusters zeigt. Hier haben Sie verschiedene Containerklassen 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, BilsterPackenthalten Sie Pillenpaare, sodass Sie die Anzahl der Paare mit 2 multiplizieren müssen. Außerdem stellen Sie möglicherweise fest, dass es sich Bottleum einen unitanderen Datentyp handelt, der umgewandelt werden muss.

In der Hauptmethode können Sie die Anzahl der Pillen mit dem folgenden 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 verletzt Single Responsibility Principle. Das bedeutet, dass Sie den Hauptmethodencode ändern müssen, wenn Sie einen neuen Containertyp hinzufügen. Es ist auch eine schlechte Praxis, den Wechsel zu verlängern.

Also durch Einführung des folgenden Codes:

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 Pills in die aufgerufene Klasse verlagert PillCountVisitor(und wir haben die switch case-Anweisung entfernt). Das heißt, wenn Sie einen neuen Pillenbehälter hinzufügen müssen, sollten Sie nur die PillCountVisitorKlasse ändern . Beachten Sie auch, dass die IVisitorBenutzeroberfläche für die Verwendung in anderen Szenarien allgemein ist.

Durch Hinzufügen der Accept-Methode zur Pillencontainerklasse:

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

    #region IAcceptor Members

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

    #endregion
}

Wir erlauben dem Besucher, Pillenbehälterklassen zu besuchen.

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

var visitor = new PillCountVisitor();

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

Das heißt: Jeder PillCountVisitorTablettenbehälter ermöglicht es dem Besucher, zu sehen, wie seine Tabletten zählen. Er weiß, wie man die Pillen zählt.

An der visitor.Counthat der Wert von Pillen.

In http://butunclebob.com/ArticleS.UncleBob.IuseVisitor sehen Sie ein reales Szenario, in dem Sie den Polymorphismus (die Antwort) nicht verwenden können , um dem Prinzip der Einzelverantwortung zu folgen. In der Tat in:

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

Die reportQtdHoursAndPayMethode dient der Berichterstattung und Darstellung und verstößt gegen das Prinzip der Einzelverantwortung. Es ist daher besser, das Besuchermuster zu verwenden, um das Problem zu lösen.

Seyed Morteza Mousavi
quelle
2
Hallo Sayed, können Sie bitte Ihre Antwort bearbeiten, um Teile hinzuzufügen, die Sie am aufschlussreichsten fanden. SO rät generell von Nur-Link-Antworten ab, da das Ziel darin besteht, eine Wissensdatenbank zu sein und Links ausfallen.
George Mauer
8

Doppelversand ist unter anderem nur ein Grund, dieses Muster zu verwenden .
Beachten Sie jedoch, dass dies die einzige Möglichkeit ist, doppelten oder mehr Versand in Sprachen zu implementieren, die ein einzelnes Versandparadigma verwenden.

Hier sind Gründe, das Muster zu verwenden:

1) Wir möchten neue Operationen definieren, ohne das Modell jedes Mal zu ändern, da sich das Modell nicht oft ändert, während sich Operationen häufig ändern.

2) Wir möchten Modell und Verhalten nicht koppeln, weil wir ein wiederverwendbares Modell in mehreren Anwendungen haben möchten oder ein erweiterbares Modell , mit dem Clientklassen ihr Verhalten mit ihren eigenen Klassen definieren können.

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

4) Wir verwenden ein Domänenmodelldesign und Modellklassen derselben Hierarchie führen zu viele verschiedene Dinge aus, die an anderer Stelle gesammelt werden könnten .

5) Wir brauchen einen doppelten Versand .
Wir haben Variablen mit Schnittstellentypen deklariert und möchten sie entsprechend ihrem Laufzeittyp verarbeiten können… natürlich ohne Verwendung if (myObj instanceof Foo) {}oder Trick.
Die Idee ist beispielsweise, 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 einen Einzelversand angewiesen sind, nicht sofort möglich, da die zur Laufzeit aufgerufene Auswahl nur vom Laufzeittyp des Empfängers abhängt.
Beachten Sie, dass in Java die aufzurufende Methode (Signatur) zur Kompilierungszeit ausgewählt wird und vom deklarierten Typ der Parameter abhängt, nicht vom Laufzeittyp.

Der letzte Punkt, der ein Grund für die Verwendung des Besuchers ist, ist auch eine Konsequenz, da Sie bei der Implementierung des Besuchers (natürlich für Sprachen, die keinen Mehrfachversand unterstützen) unbedingt eine Doppelversandimplementierung einführen müssen.

Beachten Sie, dass das Durchlaufen von Elementen (Iteration), um den Besucher auf jedes Element anzuwenden, kein Grund ist, das Muster zu verwenden.
Sie verwenden das Muster, weil Sie Modell und Verarbeitung aufteilen.
Durch die Verwendung des Musters profitieren Sie zusätzlich von einer Iterator-Fähigkeit.
Diese Fähigkeit ist sehr leistungsfähig und geht über die Iteration eines allgemeinen Typs mit einer bestimmten Methode hinaus, ebenso accept()wie eine generische Methode.
Es ist ein spezieller Anwendungsfall. Also werde ich das beiseite legen.


Beispiel in Java

Ich werde den Mehrwert des Musters anhand eines Schachbeispiels veranschaulichen, in dem wir die Verarbeitung definieren möchten, wenn der Spieler eine bewegliche Figur anfordert.

Ohne die Verwendung des Besuchermusters könnten wir das Verhalten beim Verschieben von Teilen direkt in den Unterklassen von Teilen definieren.
Wir könnten zum Beispiel eine PieceSchnittstelle haben wie:

public interface Piece{

    boolean checkMoveValidity(Coordinates coord);

    void performMove(Coordinates coord);

    Piece computeIfKingCheck();

}

Jede Piece-Unterklasse würde es implementieren wie:

public class Pawn implements Piece{

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

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

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

}

Und das Gleiche für alle Piece-Unterklassen.
Hier ist eine Diagrammklasse, die dieses Design veranschaulicht:

[Modellklassendiagramm

Dieser Ansatz weist drei wichtige Nachteile auf:

- Verhaltensweisen wie performMove()oder computeIfKingCheck()werden sehr wahrscheinlich gemeinsame Logik verwenden.
Zum Beispiel , was die konkreten Piece, performMove()wird schließlich das aktuelle Stück zu einer bestimmten Stelle gesetzt und nimmt möglicherweise den Gegner Stück.
Das Aufteilen verwandter Verhaltensweisen in mehrere Klassen, anstatt sie zu sammeln, besiegt in gewisser Weise das Muster der einzelnen Verantwortung. Ihre Wartbarkeit erschweren.

- Verarbeitung checkMoveValidity()sollte nicht etwas sein, das die PieceUnterklassen sehen oder ändern können.
Es ist eine Überprüfung, die über menschliche oder Computeraktionen hinausgeht. Diese Überprüfung wird bei jeder von einem Spieler angeforderten Aktion durchgeführt, um sicherzustellen, dass der angeforderte Spielzug gültig ist.
Das wollen wir also gar nicht in der PieceOberfläche bereitstellen.

- In Schachspielen, die für Bot-Entwickler eine Herausforderung darstellen, bietet die Anwendung im Allgemeinen eine Standard-API ( PieceSchnittstellen, Unterklassen, Board, allgemeine Verhaltensweisen usw.) und lässt Entwickler ihre Bot-Strategie bereichern.
Dazu müssen wir ein Modell vorschlagen, bei dem Daten und Verhalten in den PieceImplementierungen nicht eng miteinander verbunden sind .

Verwenden wir also das Besuchermuster!

Wir haben zwei Arten von Strukturen:

- die Modellklassen, die einen Besuch akzeptieren (die Stücke)

- die Besucher, die sie besuchen (Umzugsarbeiten)

Hier ist ein Klassendiagramm, das das Muster veranschaulicht:

Geben Sie hier die Bildbeschreibung ein

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

Hier ist die PieceMovingVisitorSchnittstelle (Verhalten für jede Art von angegeben 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);

}

Die Schlüsselmethode ist:

void accept(PieceMovingVisitor pieceVisitor);

Es bietet den ersten Versand: einen Aufruf basierend auf dem PieceEmpfänger.
Zur Kompilierungszeit ist die Methode an die accept()Methode der Piece-Schnittstelle gebunden, und zur Laufzeit wird die beschränkte Methode für die Laufzeitklasse aufgerufen Piece.
Und es ist die accept()Methodenimplementierung, die einen zweiten Versand durchführt.

In der Tat ruft jede PieceUnterklasse, die von einem PieceMovingVisitorObjekt besucht werden möchte, die PieceMovingVisitor.visit()Methode auf, indem sie selbst als Argument übergeben wird.
Auf diese Weise begrenzt der Compiler gleich zur Kompilierungszeit den Typ des deklarierten Parameters mit dem konkreten Typ.
Es gibt den zweiten Versand.
Hier ist die BishopUnterklasse, die Folgendes veranschaulicht:

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

Besucher Nachteile

Das Besuchermuster ist ein sehr leistungsfähiges Muster, weist jedoch auch einige wichtige Einschränkungen auf, die Sie berücksichtigen sollten, bevor Sie es verwenden.

1) Risiko, die Kapselung zu reduzieren / zu brechen

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

Da die MovePerformingVisitor Klasse beispielsweise die Koordinaten des tatsächlichen Stücks festlegen muss, muss die PieceSchnittstelle eine Möglichkeit bieten, dies zu tun:

void setCoordinates(Coordinates coordinates);

Die Verantwortung für PieceKoordinatenänderungen steht jetzt anderen Klassen als PieceUnterklassen offen .
Das Verschieben der vom Besucher in den PieceUnterklassen durchgeführten Verarbeitung ist ebenfalls keine Option.
Es wird in der Tat ein weiteres Problem verursachen, da Piece.accept()jede Besucherimplementierung akzeptiert wird. Es weiß nicht, was der Besucher ausführt, und daher keine Ahnung, ob und wie der Stückstatus geändert werden soll.
Eine Möglichkeit, den Besucher zu identifizieren, besteht darin, eine Nachbearbeitung Piece.accept()gemäß der Besucherimplementierung durchzuführen. Es wäre eine sehr schlechte Idee, da es eine hohe Kopplung zwischen Besuchern Implementierungen und Piece Subklassen schaffen würde und außerdem ist es wahrscheinlich zu verwenden , erfordern würde Trick wie getClass(), instanceofoder eine Markierung , um die Besucher Implementierung zu identifizieren.

2) Anforderung, das Modell zu ändern

Im Gegensatz zu einigen anderen Verhaltensentwurfsmustern wie Decoratorbeispielsweise ist das Besuchermuster aufdringlich.
Wir müssen in der Tat die anfängliche Empfängerklasse ändern, um eine accept()Methode bereitzustellen , die akzeptiert wird, um besucht zu werden.
Wir hatten kein Problem für Pieceund seine Unterklassen, da dies unsere Klassen sind .
In eingebauten Klassen oder Klassen von Drittanbietern sind die Dinge nicht so einfach.
Wir müssen sie umbrechen oder erben (wenn wir können), um die accept()Methode hinzuzufügen .

3) Indirektionen

Das Muster erzeugt mehrere Indirektionen.
Der doppelte Versand bedeutet zwei Aufrufe anstelle eines einzigen:

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

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

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

Cay Horstmann hat ein großartiges Beispiel dafür, wo Besucher in seinem OO Design- und Musterbuch angewendet werden können . Er fasst das Problem zusammen:

Zusammengesetzte Objekte haben oft eine komplexe Struktur, die aus einzelnen Elementen besteht. Einige Elemente können wieder untergeordnete Elemente haben. ... Eine Operation für ein Element besucht seine untergeordneten Elemente, wendet die Operation auf sie an und kombiniert die Ergebnisse. ... Es ist jedoch nicht einfach, einem solchen Design neue Operationen hinzuzufügen.

Der Grund, warum dies nicht einfach ist, liegt darin, dass Operationen innerhalb der Strukturklassen selbst hinzugefügt werden. Stellen Sie sich zum Beispiel vor, Sie haben ein Dateisystem:

Dateisystem-Klassendiagramm

Hier sind einige Operationen (Funktionen), die wir mit dieser Struktur implementieren möchten:

  • Zeigen Sie die Namen der Knotenelemente an (eine Dateiliste).
  • Zeigen Sie die berechnete Größe der Knotenelemente an (wobei die Größe eines Verzeichnisses die Größe aller untergeordneten Elemente enthält).
  • usw.

Sie können jeder Klasse im Dateisystem Funktionen hinzufügen, um die Operationen zu implementieren (und die Leute haben dies in der Vergangenheit getan, da es sehr offensichtlich ist, wie es geht). Das Problem ist, dass Sie den Strukturklassen möglicherweise immer mehr Methoden hinzufügen müssen, wenn Sie eine neue Funktionalität hinzufügen (die Zeile "usw." oben). Irgendwann, nach einer Reihe von Operationen, die Sie zu Ihrer Software hinzugefügt haben, sind die Methoden in diesen Klassen hinsichtlich des funktionalen Zusammenhalts der Klassen nicht mehr sinnvoll. Zum Beispiel haben Sie eine FileNode, die eine Methode hatcalculateFileColorForFunctionABC() , mit der Sie die neuesten Visualisierungsfunktionen im Dateisystem implementieren können.

Das Besuchermuster (wie viele andere Entwurfsmuster) entstand aus dem Schmerz und Leid von Entwicklern, die wussten, dass es eine bessere Möglichkeit gibt, ihren Code zu ändern, ohne dass überall viele Änderungen erforderlich sind, und dass auch gute Entwurfsprinzipien (hohe Kohäsion, geringe Kopplung) eingehalten werden ). Ich bin der Meinung, dass es schwierig ist, die Nützlichkeit vieler Muster zu verstehen, bis Sie diesen Schmerz gespürt haben. Das Erklären des Schmerzes (wie wir es oben mit den hinzugefügten "usw." -Funktionalitäten versuchen) nimmt Platz in der Erklärung ein und ist eine Ablenkung. Aus diesem Grund ist es schwierig, Muster zu verstehen.

Der Besucher ermöglicht es uns, die Funktionen der Datenstruktur (z. B. FileSystemNodes) von den Datenstrukturen selbst zu entkoppeln . Das Muster ermöglicht es dem Design, die Kohäsion zu berücksichtigen - Datenstrukturklassen sind einfacher (sie haben weniger Methoden) und auch die Funktionen sind in VisitorImplementierungen eingekapselt . Dies erfolgt über Double-Dispatching (was der komplizierte Teil des Musters ist): Verwenden von accept()Methoden in den Strukturklassen und visitX()Methoden in den Visitor-Klassen (den Funktionsklassen):

Dateisystem-Klassendiagramm mit angewendetem Besucher

Diese Struktur ermöglicht es uns, neue Funktionen hinzuzufügen, die als konkrete Besucher an der Struktur arbeiten (ohne die Strukturklassen zu ändern).

Dateisystem-Klassendiagramm mit angewendetem Besucher

Beispiel: a PrintNameVisitor, das die Verzeichnislistenfunktion implementiert, und a PrintSizeVisitor, das die Version mit der Größe implementiert. Wir könnten uns vorstellen, eines Tages einen 'ExportXMLVisitor' zu haben, der die Daten in XML generiert, oder einen anderen Besucher, der sie in JSON generiert usw. Wir könnten sogar einen Besucher haben, der meinen Verzeichnisbaum in einer grafischen Sprache wie DOT anzeigt , um ihn zu visualisieren mit einem anderen Programm.

Als letzte Anmerkung: Die Komplexität von Visitor mit seinem doppelten Versand macht es schwieriger zu verstehen, zu codieren und zu debuggen. Kurz gesagt, es hat einen hohen Geek-Faktor und widerspricht dem KISS-Prinzip. In einer von Forschern durchgeführten Umfrage wurde gezeigt, dass Besucher ein kontroverses Muster sind (es gab keinen Konsens über seine Nützlichkeit). Einige Experimente zeigten sogar, dass die Wartung des Codes dadurch nicht einfacher wurde.

Fuhrmanator
quelle
Die Verzeichnisstruktur ist meines Erachtens ein gutes zusammengesetztes Muster, stimmt aber mit Ihrem letzten Absatz überein.
Zar
5

Meiner Meinung nach ist der Arbeitsaufwand für das Hinzufügen einer neuen Operation bei Verwendung Visitor Patternoder direkter Änderung jeder Elementstruktur mehr oder weniger gleich . Wenn ich beispielsweise eine neue Elementklasse hinzufüge, Cowist die Operationsschnittstelle betroffen, und dies wird auf alle vorhandenen Elementklassen übertragen, sodass alle Elementklassen neu kompiliert werden müssen. Worum geht es also?

Kaosad
quelle
4
Fast jedes Mal, wenn ich Visitor verwendet habe, arbeiten Sie mit dem Durchlaufen einer Objekthierarchie. Betrachten Sie ein verschachteltes Baummenü. Sie möchten alle Knoten reduzieren. Wenn Sie keinen Besucher implementieren, müssen Sie einen Diagramm-Traversal-Code schreiben. Oder mit Besucher : rootElement.visit (node) -> node.collapse(). Mit Besucher implementiert jeder Knoten die Diagrammdurchquerung für alle untergeordneten Knoten, sodass Sie fertig sind.
George Mauer
@ GeorgeMauer, das Konzept des doppelten Versands hat die Motivation für mich geklärt: Entweder ist typabhängige Logik mit dem Typ oder der Welt des Schmerzes verbunden. Die Idee, die Traversal-Logik zu verteilen, lässt mich immer noch innehalten. Ist es effizienter? Ist es wartbarer? Was ist, wenn "Fold to Level N" als Voraussetzung hinzugefügt wird?
Nik.shornikov
@ nik.shornikov Effizienz sollte hier wirklich kein Problem sein. In fast jeder Sprache sind einige Funktionsaufrufe vernachlässigbar. Alles darüber hinaus ist Mikrooptimierung. Ist es wartbarer? Es hängt davon ab. Ich denke, meistens ist es das, manchmal nicht. Wie für "Falten auf Ebene N". Einfache Übergabe eines levelsRemainingZählers als Parameter. Verringern Sie es, bevor Sie die nächste Kinderstufe anrufen. Innerhalb Ihres Besuchers if(levelsRemaining == 0) return.
George Mauer
1
@ GeorgeMauer, völlig einverstanden, dass Effizienz ein untergeordnetes Problem ist. Aber die Wartbarkeit, z. B. das Überschreiben der akzeptierten Signatur, ist genau das, worauf sich die Entscheidung meiner Meinung nach beschränken sollte.
Nik.shornikov
5

Besuchermuster als dieselbe unterirdische Implementierung der Aspect Object-Programmierung.

Zum Beispiel, wenn Sie eine neue Operation definieren, ohne die Klassen der Elemente zu ändern, mit denen sie arbeitet

Mischungz
quelle
bis zur Erwähnung von Aspect Object Programming
Milesma
5

Schnelle Beschreibung des Besuchermusters. Die Klassen, die geändert werden müssen, müssen alle die Methode 'accept' implementieren. Clients rufen diese Akzeptanzmethode auf, um eine neue Aktion für diese Klassenfamilie auszuführen und dadurch ihre Funktionalität zu erweitern. Clients können diese One-Accept-Methode verwenden, um eine Vielzahl neuer Aktionen auszuführen, indem sie für jede bestimmte Aktion eine andere Besucherklasse übergeben. Eine Besucherklasse enthält mehrere überschriebene Besuchsmethoden, die definieren, wie dieselbe spezifische Aktion für jede Klasse innerhalb der Familie ausgeführt werden soll. Diese Besuchsmethoden erhalten eine Instanz, an der gearbeitet werden soll.

Wenn Sie es in Betracht ziehen könnten

  1. Wenn Sie eine Klassenfamilie haben, wissen Sie, dass Sie viele neue Aktionen hinzufügen müssen, aber aus irgendeinem Grund können Sie die Klassenfamilie in Zukunft nicht mehr ändern oder neu kompilieren.
  2. Wenn Sie eine neue Aktion hinzufügen möchten und diese neue Aktion vollständig innerhalb einer Besucherklasse definiert haben möchten, anstatt sich auf mehrere Klassen zu verteilen.
  3. Wenn Ihr Chef sagt , müssen Sie eine Reihe von Klassen erzeugen , die etwas tun müssen , gerade jetzt ! ... aber eigentlich niemand weiß genau , was das etwas noch ist.
Andrew Pate
quelle
4

Ich habe dieses Muster nicht verstanden, bis ich auf Onkel Bob Artikel stieß und Kommentare las. Betrachten Sie den folgenden Code:

public class Employee
{
}

public class SalariedEmployee : Employee
{
}

public class HourlyEmployee : Employee
{
}

public class QtdHoursAndPayReport
{
    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        foreach (Employee e in employees)
        {
            if (e is HourlyEmployee he)
                PrintReportLine(he);
            if (e is SalariedEmployee se)
                PrintReportLine(se);
        }
    }

    public void PrintReportLine(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hours");
    }
    public void PrintReportLine(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    }
}

class Program
{
    static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }
}

Obwohl es gut aussehen mag, da es die Einzelverantwortung bestätigt, verstößt es gegen das Open / Closed- Prinzip. Jedes Mal, wenn Sie einen neuen Mitarbeitertyp haben, müssen Sie diesen hinzufügen, wenn Sie eine Typprüfung durchführen. Und wenn Sie es nicht tun, werden Sie das beim Kompilieren nie erfahren.

Mit dem Besuchermuster können Sie Ihren Code sauberer machen, da er nicht gegen das Open / Closed-Prinzip und nicht gegen die Einzelverantwortung verstößt. Und wenn Sie vergessen, visit zu implementieren, wird es nicht kompiliert:

public abstract class Employee
{
    public abstract void Accept(EmployeeVisitor v);
}

public class SalariedEmployee : Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public class HourlyEmployee:Employee
{
    public override void Accept(EmployeeVisitor v)
    {
        v.Visit(this);
    }
}

public interface EmployeeVisitor
{
    void Visit(HourlyEmployee he);
    void Visit(SalariedEmployee se);
}

public class QtdHoursAndPayReport : EmployeeVisitor
{
    public void Visit(HourlyEmployee he)
    {
        System.Diagnostics.Debug.WriteLine("hourly");
        // generate the line of the report.
    }
    public void Visit(SalariedEmployee se)
    {
        System.Diagnostics.Debug.WriteLine("fix");
    } // do nothing

    public void PrintReport()
    {
        var employees = new List<Employee>
        {
            new SalariedEmployee(),
            new HourlyEmployee()
        };
        QtdHoursAndPayReport v = new QtdHoursAndPayReport();
        foreach (var emp in employees)
        {
            emp.Accept(v);
        }
    }
}

class Program
{

    public static void Main(string[] args)
    {
        new QtdHoursAndPayReport().PrintReport();
    }       
}  
}

Die Magie ist, dass es zwar gleich v.Visit(this)aussieht, aber tatsächlich anders ist, da es unterschiedliche Besucherüberladungen hervorruft.

Zugriff abgelehnt
quelle
Ja, ich finde es besonders nützlich, wenn ich mit Baumstrukturen arbeite, nicht nur mit flachen Listen (flache Listen wären ein Sonderfall eines Baumes). Wie Sie bemerken, ist es nicht nur auf Listen furchtbar chaotisch, aber Besucher können ein Retter sein, wenn die Navigation zwischen Knoten komplexer wird
George Mauer
3

Basierend auf der hervorragenden Antwort von @Federico A. Ramponi.

Stellen Sie sich vor, Sie haben diese Hierarchie:

public interface IAnimal
{
    void DoSound();
}

public class Dog : IAnimal
{
    public void DoSound()
    {
        Console.WriteLine("Woof");
    }
}

public class Cat : IAnimal
{
    public void DoSound(IOperation o)
    {
        Console.WriteLine("Meaw");
    }
}

Was passiert, wenn Sie hier eine "Walk" -Methode hinzufügen müssen? Das wird für das gesamte Design schmerzhaft sein.

Gleichzeitig werden durch Hinzufügen der "Walk" -Methode neue Fragen generiert. Was ist mit "Essen" oder "Schlafen"? Müssen wir der Tierhierarchie wirklich für jede neue Aktion oder Operation, die wir hinzufügen möchten, eine neue Methode hinzufügen? Das ist hässlich und am wichtigsten, wir werden niemals in der Lage sein, die Animal-Oberfläche zu schließen. Mit dem Besuchermuster können wir der Hierarchie eine neue Methode hinzufügen, ohne die Hierarchie zu ändern!

Überprüfen Sie einfach dieses C # -Beispiel und führen Sie es aus:

using System;
using System.Collections.Generic;

namespace VisitorPattern
{
    class Program
    {
        static void Main(string[] args)
        {
            var animals = new List<IAnimal>
            {
                new Cat(), new Cat(), new Dog(), new Cat(), 
                new Dog(), new Dog(), new Cat(), new Dog()
            };

            foreach (var animal in animals)
            {
                animal.DoOperation(new Walk());
                animal.DoOperation(new Sound());
            }

            Console.ReadLine();
        }
    }

    public interface IOperation
    {
        void PerformOperation(Dog dog);
        void PerformOperation(Cat cat);
    }

    public class Walk : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Dog walking");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Cat Walking");
        }
    }

    public class Sound : IOperation
    {
        public void PerformOperation(Dog dog)
        {
            Console.WriteLine("Woof");
        }

        public void PerformOperation(Cat cat)
        {
            Console.WriteLine("Meaw");
        }
    }

    public interface IAnimal
    {
        void DoOperation(IOperation o);
    }

    public class Dog : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }

    public class Cat : IAnimal
    {
        public void DoOperation(IOperation o)
        {
            o.PerformOperation(this);
        }
    }
}
Tomás Escamez
quelle
Gehen, Essen sind keine geeigneten Beispiele, da sie sowohl Dogals auch gemeinsam sind Cat. Sie hätten sie in der Basisklasse erstellen können, damit sie vererbt werden, oder ein geeignetes Beispiel auswählen können.
Abhinav Gauniyal
Klänge sind anders, gutes Sample, aber unsicher, ob es etwas mit dem Besuchermuster zu tun hat
DAG
3

Besucher

Mit Visitor können Sie einer Klassenfamilie neue virtuelle Funktionen hinzufügen, ohne die Klassen selbst zu ändern. Stattdessen wird eine Besucherklasse erstellt, die alle entsprechenden Spezialisierungen der virtuellen Funktion implementiert

Besucherstruktur:

Geben Sie hier die Bildbeschreibung ein

Verwenden Sie das Besuchermuster, wenn:

  1. Ähnliche Operationen müssen für Objekte verschiedener Typen ausgeführt werden, die in einer Struktur gruppiert sind
  2. Sie müssen viele verschiedene und nicht verwandte Vorgänge ausführen. Es trennt Operation von Objektstruktur
  3. Neue Operationen müssen ohne Änderung der Objektstruktur hinzugefügt werden
  4. Sammeln Sie verwandte Operationen in einer einzigen Klasse, anstatt Sie zu zwingen, Klassen zu ändern oder abzuleiten
  5. Fügen Sie Funktionen zu Klassenbibliotheken hinzu, für die Sie entweder nicht über die Quelle verfügen oder die Quelle nicht ändern können

Obwohl das Besuchermuster Flexibilität bietet, um neue Operationen hinzuzufügen, ohne den vorhandenen Code in Object zu ändern, hat diese Flexibilität einen Nachteil.

Wenn ein neues Visitable-Objekt hinzugefügt wurde, sind Codeänderungen in den Visitor & ConcreteVisitor-Klassen erforderlich . Es gibt eine Problemumgehung, um dieses Problem zu beheben: Verwenden Sie Reflexion, die sich auf die Leistung auswirkt.

Code-Auszug:

import java.util.HashMap;

interface Visitable{
    void accept(Visitor visitor);
}

interface Visitor{
    void logGameStatistics(Chess chess);
    void logGameStatistics(Checkers checkers);
    void logGameStatistics(Ludo ludo);    
}
class GameVisitor implements Visitor{
    public void logGameStatistics(Chess chess){
        System.out.println("Logging Chess statistics: Game Completion duration, number of moves etc..");    
    }
    public void logGameStatistics(Checkers checkers){
        System.out.println("Logging Checkers statistics: Game Completion duration, remaining coins of loser");    
    }
    public void logGameStatistics(Ludo ludo){
        System.out.println("Logging Ludo statistics: Game Completion duration, remaining coins of loser");    
    }
}

abstract class Game{
    // Add game related attributes and methods here
    public Game(){

    }
    public void getNextMove(){};
    public void makeNextMove(){}
    public abstract String getName();
}
class Chess extends Game implements Visitable{
    public String getName(){
        return Chess.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Checkers extends Game implements Visitable{
    public String getName(){
        return Checkers.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}
class Ludo extends Game implements Visitable{
    public String getName(){
        return Ludo.class.getName();
    }
    public void accept(Visitor visitor){
        visitor.logGameStatistics(this);
    }
}

public class VisitorPattern{
    public static void main(String args[]){
        Visitor visitor = new GameVisitor();
        Visitable games[] = { new Chess(),new Checkers(), new Ludo()};
        for (Visitable v : games){
            v.accept(visitor);
        }
    }
}

Erläuterung:

  1. Visitable( Element) ist eine Schnittstelle und diese Schnittstellenmethode muss einer Reihe von Klassen hinzugefügt werden.
  2. Visitorist eine Schnittstelle, die Methoden zum Ausführen einer Operation für VisitableElemente enthält.
  3. GameVisitorist eine Klasse, die Visitorinterface ( ConcreteVisitor) implementiert .
  4. Jedes VisitableElement akzeptiert Visitorund ruft eine relevante VisitorSchnittstellenmethode auf.
  5. Sie können behandeln Gamewie Elementund konkrete Spiele wie Chess,Checkers and Ludowie ConcreteElements.

Im obigen Beispiel Chess, Checkers and Ludosind drei verschiedene Spiele (und VisitableKlassen). An einem schönen Tag bin ich auf ein Szenario gestoßen, in dem Statistiken für jedes Spiel protokolliert werden. Ohne die einzelnen Klassen zu ändern, um Statistikfunktionen zu implementieren, können Sie diese Verantwortung in der GameVisitorKlasse zentralisieren. Dies erledigt den Trick für Sie, ohne die Struktur jedes Spiels zu ändern.

Ausgabe:

Logging Chess statistics: Game Completion duration, number of moves etc..
Logging Checkers statistics: Game Completion duration, remaining coins of loser
Logging Ludo statistics: Game Completion duration, remaining coins of loser

Beziehen auf

oodesign Artikel

Artikel zur Herstellung von Quellen

für mehr Details

Dekorateur

Mit Muster kann einem einzelnen Objekt entweder statisch oder dynamisch Verhalten hinzugefügt werden, ohne das Verhalten anderer Objekte derselben Klasse zu beeinflussen

Zusammenhängende Posts:

Dekorationsmuster für E / A.

Wann sollte das Dekorationsmuster verwendet werden?

Ravindra Babu
quelle
2

Die Beschreibung und das Beispiel von http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html gefallen mir sehr gut .

Die Annahme ist, dass Sie eine feste Primärklassenhierarchie haben. Möglicherweise stammt es von einem anderen Anbieter, und Sie können keine Änderungen an dieser Hierarchie vornehmen. Sie möchten dieser Hierarchie jedoch neue polymorphe Methoden hinzufügen, was bedeutet, dass Sie normalerweise der Basisklassenschnittstelle etwas hinzufügen müssen. Das Dilemma besteht also darin, dass Sie der Basisklasse Methoden hinzufügen müssen, die Basisklasse jedoch nicht berühren können. Wie kommst du darum herum?

Das Entwurfsmuster, das diese Art von Problem löst, wird als "Besucher" bezeichnet (das letzte im Buch "Entwurfsmuster") und baut auf dem im letzten Abschnitt gezeigten Doppelversandschema auf.

Mit dem Besuchermuster können Sie die Schnittstelle des Primärtyps erweitern, indem Sie eine separate Klassenhierarchie vom Typ Besucher erstellen, um die für den Primärtyp ausgeführten Vorgänge zu virtualisieren. Die Objekte des Primärtyps "akzeptieren" einfach den Besucher und rufen dann die dynamisch gebundene Mitgliedsfunktion des Besuchers auf.

wojcikstefan
quelle
Während es sich technisch gesehen um das Besuchermuster handelt, handelt es sich bei diesem Beispiel nur um einen einfachen Doppelversand. Ich würde argumentieren, dass die Nützlichkeit allein dadurch nicht besonders sichtbar wird.
George Mauer
1

Während ich das Wie und Wann verstanden habe, habe ich das Warum nie verstanden. Falls es jemandem mit einem Hintergrund in einer Sprache wie C ++ hilft, sollten Sie dies sehr sorgfältig lesen .

Für die Faulen verwenden wir das Besuchermuster, weil "während virtuelle Funktionen in C ++ dynamisch ausgeliefert werden, die Funktionsüberladung statisch erfolgt" .

Oder anders ausgedrückt: Stellen Sie sicher, dass CollideWith (ApolloSpacecraft &) aufgerufen wird, wenn Sie eine SpaceShip-Referenz übergeben, die tatsächlich an ein ApolloSpacecraft-Objekt gebunden ist.

class SpaceShip {};
class ApolloSpacecraft : public SpaceShip {};
class ExplodingAsteroid : public Asteroid {
public:
  virtual void CollideWith(SpaceShip&) {
    cout << "ExplodingAsteroid hit a SpaceShip" << endl;
  }
  virtual void CollideWith(ApolloSpacecraft&) {
    cout << "ExplodingAsteroid hit an ApolloSpacecraft" << endl;
  }
}
Carl
quelle
2
Die Verwendung des dynamischen Versands im Besuchermuster verwirrt mich völlig. Vorgeschlagene Verwendungen des Musters beschreiben die Verzweigung, die zur Kompilierungszeit durchgeführt werden kann. Diese Fälle wären mit einer Funktionsvorlage anscheinend besser dran.
Praxeolitic
0

Vielen Dank für die großartige Erklärung von @Federico A. Ramponi , ich habe dies gerade in der Java- Version gemacht. Hoffe es könnte hilfreich sein.

Genau wie @Konrad Rudolph betonte, handelt es sich tatsächlich um einen doppelten Versand, bei dem zwei konkrete Instanzen zusammen verwendet werden, um die Laufzeitmethoden zu bestimmen.

Es ist also eigentlich nicht erforderlich, eine gemeinsame Schnittstelle für den Operations Executor zu erstellen, solange die Operationsschnittstelle richtig definiert ist.

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showTheHobby(food);
        Katherine katherine = new Katherine();
        katherine.presentHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void embed(Katherine katherine);
}


class Hearen {
    String name = "Hearen";
    void showTheHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine {
    String name = "Katherine";
    void presentHobby(Hobby hobby) {
        hobby.embed(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void embed(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}

Wie Sie erwarten, bringt uns eine gemeinsame Benutzeroberfläche mehr Klarheit, obwohl dies eigentlich nicht der wesentliche Teil dieses Musters ist.

import static java.lang.System.out;
public class Visitor_2 {
    public static void main(String...args) {
        Hearen hearen = new Hearen();
        FoodImpl food = new FoodImpl();
        hearen.showHobby(food);
        Katherine katherine = new Katherine();
        katherine.showHobby(food);
    }
}

interface Hobby {
    void insert(Hearen hearen);
    void insert(Katherine katherine);
}

abstract class Person {
    String name;
    protected Person(String n) {
        this.name = n;
    }
    abstract void showHobby(Hobby hobby);
}

class Hearen extends  Person {
    public Hearen() {
        super("Hearen");
    }
    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class Katherine extends Person {
    public Katherine() {
        super("Katherine");
    }

    @Override
    void showHobby(Hobby hobby) {
        hobby.insert(this);
    }
}

class FoodImpl implements Hobby {
    public void insert(Hearen hearen) {
        out.println(hearen.name + " start to eat bread");
    }
    public void insert(Katherine katherine) {
        out.println(katherine.name + " start to eat mango");
    }
}
Gehört
quelle
0

Ihre Frage ist, wann Sie wissen müssen:

Ich codiere nicht zuerst mit dem Besuchermuster. Ich codiere Standard und warte auf die Notwendigkeit und dann Refactor. Nehmen wir also an, Sie haben mehrere Zahlungssysteme, die Sie gleichzeitig installiert haben. Beim Auschecken können Sie viele if-Bedingungen (oder instanceOf) haben, zum Beispiel:

//psuedo code
    if(payPal) 
    do paypal checkout 
    if(stripe)
    do strip stuff checkout
    if(payoneer)
    do payoneer checkout

Jetzt stell dir vor, ich hätte 10 Zahlungsmethoden, es wird irgendwie hässlich. Wenn Sie also sehen, dass ein solches Muster auftritt, kommt der Besucher herein, um all das zu trennen, und Sie rufen danach so etwas an:

new PaymentCheckoutVistor(paymentType).visit()

Sie können anhand der Anzahl der Beispiele hier sehen, wie Sie es implementieren können. Ich zeige Ihnen nur einen Anwendungsfall.

j2emanue
quelle