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.
design-patterns
visitor-pattern
George Mauer
quelle
quelle
Antworten:
Ich bin mit dem Besuchermuster nicht sehr vertraut. Mal sehen, ob ich es richtig verstanden habe. Angenommen, Sie haben eine Hierarchie von Tieren
(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:
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:
Dann ändern wir die Hierarchie, um neue Operationen zu akzeptieren:
Schließlich implementieren wir die eigentliche Operation, ohne weder Katze noch Hund zu ändern :
Jetzt haben Sie die Möglichkeit, Operationen hinzuzufügen, ohne die Hierarchie mehr zu ändern. So funktioniert es:
quelle
letsDo(Operation *v)
braucht einen Zeiger.theSound.hereIsACat(c)
Hätte die Arbeit erledigt, wie rechtfertigen Sie den gesamten durch das Muster verursachten Overhead? Doppelversand ist die Rechtfertigung.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 .
quelle
switch
:switch
hart-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.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).Jeder hier hat Recht, aber ich denke, es wird das "Wann" nicht angesprochen. Zunächst aus Design Patterns:
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.
quelle
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.]
Die
visit
Methode wendet auf jeden Knoten in der Struktur ein Besucherobjekt an. In diesem Fall ist es ein Top-Down-Besucher. Sie können die Struktur dervisit
Methode ändern , um Bottom-up oder eine andere Reihenfolge zu erreichen.Hier ist eine Superklasse für Besucher. Es wird von der
visit
Methode verwendet. Es "erreicht" jeden Knoten in der Struktur. Da dievisit
Methodeup
und aufruftdown
, kann der Besucher die Tiefe verfolgen.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 eineVisitor
,dumpNodes
.Dann gilt das
dumpNodes
für den Baum. DasdumpNode
Objekt "besucht" jeden Knoten im Baum.Der TreeNode-
visit
Algorithmus stellt sicher , dass jeder TreeNode als Argument für die BesuchermethodearrivedAt
verwendet wird.quelle
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.)
quelle
Es gibt mindestens drei sehr gute Gründe für die Verwendung des Besuchermusters:
Reduzieren Sie die Verbreitung von Code, die sich nur geringfügig unterscheidet, wenn sich Datenstrukturen ändern.
Wenden Sie dieselbe Berechnung auf mehrere Datenstrukturen an, ohne den Code zu ändern, der die Berechnung implementiert.
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 .
quelle
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 -
Jetzt möchte ich eine Operation vorstellen - Bluetooth auf einem mobilen Gerät einschalten.
Die Funktionssignatur sollte ungefähr so aussehen -
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.
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 -
Ich schrieb das Beispiel eine andere Frage zu beantworten, der Code seine Erklärung erwähnt hier .
quelle
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
:Wie Sie oben sehen,
BilsterPack
enthalten Sie Pillenpaare, sodass Sie die Anzahl der Paare mit 2 multiplizieren müssen. Außerdem stellen Sie möglicherweise fest, dass es sichBottle
um einenunit
anderen Datentyp handelt, der umgewandelt werden muss.In der Hauptmethode können Sie die Anzahl der Pillen mit dem folgenden Code berechnen:
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:
Sie haben die Verantwortung für das Zählen der Anzahl von
Pill
s in die aufgerufene Klasse verlagertPillCountVisitor
(und wir haben die switch case-Anweisung entfernt). Das heißt, wenn Sie einen neuen Pillenbehälter hinzufügen müssen, sollten Sie nur diePillCountVisitor
Klasse ändern . Beachten Sie auch, dass dieIVisitor
Benutzeroberfläche für die Verwendung in anderen Szenarien allgemein ist.Durch Hinzufügen der Accept-Methode zur Pillencontainerklasse:
Wir erlauben dem Besucher, Pillenbehälterklassen zu besuchen.
Am Ende berechnen wir die Anzahl der Pillen mit folgendem Code:
Das heißt: Jeder
PillCountVisitor
Tablettenbehälter ermöglicht es dem Besucher, zu sehen, wie seine Tabletten zählen. Er weiß, wie man die Pillen zählt.An der
visitor.Count
hat 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:
Die
reportQtdHoursAndPay
Methode 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.quelle
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
Piece
Schnittstelle haben wie:Jede Piece-Unterklasse würde es implementieren wie:
Und das Gleiche für alle Piece-Unterklassen.
Hier ist eine Diagrammklasse, die dieses Design veranschaulicht:
Dieser Ansatz weist drei wichtige Nachteile auf:
- Verhaltensweisen wie
performMove()
odercomputeIfKingCheck()
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 diePiece
Unterklassen 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
Piece
Oberfläche bereitstellen.- In Schachspielen, die für Bot-Entwickler eine Herausforderung darstellen, bietet die Anwendung im Allgemeinen eine Standard-API (
Piece
Schnittstellen, 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
Piece
Implementierungen 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:
Im oberen Teil haben wir die Besucher und im unteren Teil haben wir die Modellklassen.
Hier ist die
PieceMovingVisitor
Schnittstelle (Verhalten für jede Art von angegebenPiece
):Das Stück ist jetzt definiert:
Die Schlüsselmethode ist:
Es bietet den ersten Versand: einen Aufruf basierend auf dem
Piece
Empfä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 aufgerufenPiece
.Und es ist die
accept()
Methodenimplementierung, die einen zweiten Versand durchführt.In der Tat ruft jede
Piece
Unterklasse, die von einemPieceMovingVisitor
Objekt besucht werden möchte, diePieceMovingVisitor.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
Bishop
Unterklasse, die Folgendes veranschaulicht:Und hier ein Anwendungsbeispiel:
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 diePiece
Schnittstelle eine Möglichkeit bieten, dies zu tun:Die Verantwortung für
Piece
Koordinatenänderungen steht jetzt anderen Klassen alsPiece
Unterklassen offen .Das Verschieben der vom Besucher in den
Piece
Unterklassen 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 wiegetClass()
,instanceof
oder eine Markierung , um die Besucher Implementierung zu identifizieren.2) Anforderung, das Modell zu ändern
Im Gegensatz zu einigen anderen Verhaltensentwurfsmustern wie
Decorator
beispielsweise 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
Piece
und 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:
Und wir könnten zusätzliche Indirektionen haben, wenn der Besucher den Status des besuchten Objekts ändert.
Es kann wie ein Zyklus aussehen:
quelle
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:
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:
Hier sind einige Operationen (Funktionen), die wir mit dieser Struktur implementieren möchten:
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 inVisitor
Implementierungen eingekapselt . Dies erfolgt über Double-Dispatching (was der komplizierte Teil des Musters ist): Verwenden vonaccept()
Methoden in den Strukturklassen undvisitX()
Methoden in den Visitor-Klassen (den Funktionsklassen):Diese Struktur ermöglicht es uns, neue Funktionen hinzuzufügen, die als konkrete Besucher an der Struktur arbeiten (ohne die Strukturklassen zu ändern).
Beispiel: a
PrintNameVisitor
, das die Verzeichnislistenfunktion implementiert, und aPrintSizeVisitor
, 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.
quelle
Meiner Meinung nach ist der Arbeitsaufwand für das Hinzufügen einer neuen Operation bei Verwendung
Visitor Pattern
oder direkter Änderung jeder Elementstruktur mehr oder weniger gleich . Wenn ich beispielsweise eine neue Elementklasse hinzufüge,Cow
ist die Operationsschnittstelle betroffen, und dies wird auf alle vorhandenen Elementklassen übertragen, sodass alle Elementklassen neu kompiliert werden müssen. Worum geht es also?quelle
rootElement.visit (node) -> node.collapse()
. Mit Besucher implementiert jeder Knoten die Diagrammdurchquerung für alle untergeordneten Knoten, sodass Sie fertig sind.levelsRemaining
Zählers als Parameter. Verringern Sie es, bevor Sie die nächste Kinderstufe anrufen. Innerhalb Ihres Besuchersif(levelsRemaining == 0) return
.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
quelle
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
quelle
Ich habe dieses Muster nicht verstanden, bis ich auf Onkel Bob Artikel stieß und Kommentare las. Betrachten Sie den folgenden Code:
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:
Die Magie ist, dass es zwar gleich
v.Visit(this)
aussieht, aber tatsächlich anders ist, da es unterschiedliche Besucherüberladungen hervorruft.quelle
Basierend auf der hervorragenden Antwort von @Federico A. Ramponi.
Stellen Sie sich vor, Sie haben diese Hierarchie:
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:
quelle
Dog
als auch gemeinsam sindCat
. Sie hätten sie in der Basisklasse erstellen können, damit sie vererbt werden, oder ein geeignetes Beispiel auswählen können.Besucher
Besucherstruktur:
Verwenden Sie das Besuchermuster, wenn:
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:
Erläuterung:
Visitable
(Element
) ist eine Schnittstelle und diese Schnittstellenmethode muss einer Reihe von Klassen hinzugefügt werden.Visitor
ist eine Schnittstelle, die Methoden zum Ausführen einer Operation fürVisitable
Elemente enthält.GameVisitor
ist eine Klasse, dieVisitor
interface (ConcreteVisitor
) implementiert .Visitable
Element akzeptiertVisitor
und ruft eine relevanteVisitor
Schnittstellenmethode auf.Game
wieElement
und konkrete Spiele wieChess,Checkers and Ludo
wieConcreteElements
.Im obigen Beispiel
Chess, Checkers and Ludo
sind drei verschiedene Spiele (undVisitable
Klassen). 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 derGameVisitor
Klasse zentralisieren. Dies erledigt den Trick für Sie, ohne die Struktur jedes Spiels zu ändern.Ausgabe:
Beziehen auf
oodesign Artikel
Artikel zur Herstellung von Quellen
für mehr Details
Dekorateur
Zusammenhängende Posts:
Dekorationsmuster für E / A.
Wann sollte das Dekorationsmuster verwendet werden?
quelle
Die Beschreibung und das Beispiel von http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Visitor.html gefallen mir sehr gut .
quelle
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.
quelle
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.
Wie Sie erwarten, bringt uns eine gemeinsame Benutzeroberfläche mehr Klarheit, obwohl dies eigentlich nicht der wesentliche Teil dieses Musters ist.
quelle
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:
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:
Sie können anhand der Anzahl der Beispiele hier sehen, wie Sie es implementieren können. Ich zeige Ihnen nur einen Anwendungsfall.
quelle