Es wird viel darüber geredet, die Algorithmen von den Klassen zu entkoppeln. Eines bleibt jedoch nicht erklärt.
Sie benutzen Besucher wie diesen
abstract class Expr {
public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}
class ExprVisitor extends Visitor{
public Integer visit(Num num) {
return num.value;
}
public Integer visit(Sum sum) {
return sum.getLeft().accept(this) + sum.getRight().accept(this);
}
public Integer visit(Prod prod) {
return prod.getLeft().accept(this) * prod.getRight().accept(this);
}
Anstatt visit (element) direkt aufzurufen, fordert Visitor das Element auf, seine visit-Methode aufzurufen. Es widerspricht der erklärten Idee der Klassenunwissenheit über Besucher.
PS1 Bitte erkläre es mit deinen eigenen Worten oder zeige auf eine genaue Erklärung. Weil sich zwei Antworten auf etwas Allgemeines und Ungewisses beziehen.
PS2 Meine Vermutung: Da getLeft()
das Basic zurückgegeben Expression
wird, visit(getLeft())
würde ein Anruf dazu führen visit(Expression)
, während ein getLeft()
Anruf visit(this)
zu einem anderen, angemesseneren Besuchsaufruf führt. So,accept()
führt die Typumwandlung (aka Gießen).
PS3 Scalas Pattern Matching = Besuchermuster auf Steroid zeigt, wie viel einfacher das Besuchermuster ohne die Akzeptanzmethode ist. Wikipedia fügt dieser Aussage hinzu : indem ein Artikel verknüpft wird, der zeigt, "dass accept()
Methoden nicht erforderlich sind, wenn Reflexion verfügbar ist; führt den Begriff" Walkabout "für die Technik ein".
Antworten:
Das Besuchermuster
visit
/accept
Konstrukte sind ein notwendiges Übel aufgrund von C-ähnliche Sprachen (C #, Java, etc.) Semantik. Das Ziel des Besuchermusters besteht darin, Ihren Anruf mithilfe des doppelten Versands so weiterzuleiten, wie Sie es vom Lesen des Codes erwarten.Normalerweise ist bei Verwendung des Besuchermusters eine Objekthierarchie beteiligt, bei der alle Knoten von einem Basistyp abgeleitet sind
Node
, der im Folgenden als bezeichnet wirdNode
. Instinktiv würden wir es so schreiben:Hierin liegt das Problem. Wenn unsere
MyVisitor
Klasse wie folgt definiert wurde:Wenn zur Laufzeit, unabhängig vom tatsächlichen Typ
root
, unser Aufruf in die Überlastung gehen würdevisit(Node node)
. Dies gilt für alle vom Typ deklarierten VariablenNode
. Warum ist das? Da Java und andere C-ähnliche Sprachen bei der Entscheidung, welche Überladung aufgerufen werden soll, nur den statischen Typ oder den Typ berücksichtigen, als den die Variable deklariert ist. Java unternimmt nicht den zusätzlichen Schritt, um bei jedem Methodenaufruf zur Laufzeit zu fragen: "Okay, was ist der dynamische Typroot
? Oh, ich verstehe. Es ist einTrainNode
. Mal sehen, ob es eine Methode inMyVisitor
die einen Parameter vom Typ akzeptiertTrainNode
... ". Der Compiler bestimmt zur Kompilierungszeit, welche Methode aufgerufen wird. (Wenn Java tatsächlich die dynamischen Typen der Argumente untersuchen würde, wäre die Leistung ziemlich schrecklich.)Java bietet uns ein Tool, mit dem der Laufzeittyp (dh der dynamische Typ) eines Objekts beim Aufruf einer Methode berücksichtigt werden kann - der Versand virtueller Methoden . Wenn wir eine virtuelle Methode aufrufen, wird der Aufruf tatsächlich an eine Tabelle im Speicher gesendet, die aus Funktionszeigern besteht. Jeder Typ hat eine Tabelle. Wenn eine bestimmte Methode von einer Klasse überschrieben wird, enthält der Funktionstabelleneintrag dieser Klasse die Adresse der überschriebenen Funktion. Wenn die Klasse eine Methode nicht überschreibt, enthält sie einen Zeiger auf die Implementierung der Basisklasse. Dies verursacht immer noch einen Leistungsaufwand (bei jedem Methodenaufruf werden im Wesentlichen zwei Zeiger dereferenziert: einer zeigt auf die Funktionstabelle des Typs und einer auf die Funktion selbst), aber es ist immer noch schneller, als Parametertypen untersuchen zu müssen.
Das Ziel des Besuchermusters ist es, einen doppelten Versand zu erreichen - wird nicht nur der Typ des Anrufziels berücksichtigt (
MyVisitor
über virtuelle Methoden), sondern auch der Typ des Parameters (welche Art von betrachtenNode
wir)? Das Besuchermuster ermöglicht es uns, dies durch die Kombinationvisit
/ zu tunaccept
.Indem Sie unsere Linie dahingehend ändern:
Wir können bekommen, was wir wollen: Über den Versand virtueller Methoden geben wir den richtigen accept () -Aufruf ein, wie er von der Unterklasse implementiert wurde. In unserem Beispiel mit geben
TrainElement
wir dieTrainElement
Implementierung von einaccept()
:Was weiß der Compiler an dieser Stelle im Rahmen von
TrainNode
'saccept
? Es weiß, dass der statische Typ von athis
istTrainNode
. Dies ist eine wichtige zusätzliche Information, die dem Compiler im Bereich unseres Aufrufers nicht bekannt war: Dort wusste erroot
nur, dass es sich um eine handeltNode
. Jetzt weiß der Compiler, dassthis
(root
) nicht nur einNode
, sondern tatsächlich ein istTrainNode
. Infolgedessen bedeutet die eine Zeile im Innerenaccept()
:v.visit(this)
etwas ganz anderes. Der Compiler sucht nun nach einer Überladungvisit()
, die a benötigtTrainNode
. Wenn es keinen findet, kompiliert es den Aufruf zu einer Überladung, die a benötigtNode
. Wenn beides nicht vorhanden ist, wird ein Kompilierungsfehler angezeigt (es sei denn, Sie haben eine Überladung, die erforderlich istobject
). Die Ausführung wird somit in das eingehen, was wir die ganze Zeit beabsichtigt hatten:MyVisitor
die Implementierung vonvisit(TrainNode e)
. Es waren keine Abgüsse erforderlich, und vor allem war keine Reflexion erforderlich. Daher ist der Aufwand für diesen Mechanismus eher gering: Er besteht nur aus Zeigerreferenzen und sonst nichts.Sie haben Recht mit Ihrer Frage - wir können eine Besetzung verwenden und das richtige Verhalten erzielen. Oft wissen wir jedoch nicht einmal, um welchen Knotentyp es sich handelt. Nehmen Sie den Fall der folgenden Hierarchie:
Und wir haben einen einfachen Compiler geschrieben, der eine Quelldatei analysiert und eine Objekthierarchie erzeugt, die der obigen Spezifikation entspricht. Wenn wir einen Interpreter für die als Besucher implementierte Hierarchie schreiben würden:
Casting würde uns nicht sehr weit bringen, da wir die Arten
left
oderright
in denvisit()
Methoden nicht kennen . Unser Parser würde höchstwahrscheinlich auch nur ein Objekt vom Typ zurückgeben,Node
das ebenfalls auf die Wurzel der Hierarchie zeigt, sodass wir dies auch nicht sicher umsetzen können. Unser einfacher Dolmetscher kann also so aussehen:Das Besuchermuster ermöglicht es uns, etwas sehr Mächtiges zu tun: Bei einer gegebenen Objekthierarchie können wir modulare Operationen erstellen, die über die Hierarchie arbeiten, ohne dass der Code in die Klasse der Hierarchie selbst eingefügt werden muss. Das Besuchermuster wird beispielsweise in der Compilerkonstruktion häufig verwendet. In Anbetracht des Syntaxbaums eines bestimmten Programms werden viele Besucher geschrieben, die diesen Baum bearbeiten: Typprüfung, Optimierungen und Emission von Maschinencode werden normalerweise als unterschiedliche Besucher implementiert. Im Falle des Optimierungsbesuchers kann er anhand des Eingabebaums sogar einen neuen Syntaxbaum ausgeben.
Das hat natürlich seine Nachteile: Wenn wir der Hierarchie einen neuen Typ hinzufügen, müssen wir
visit()
derIVisitor
Schnittstelle auch eine Methode für diesen neuen Typ hinzufügen und bei allen unseren Besuchern Stub- (oder vollständige) Implementierungen erstellen.accept()
Aus den oben beschriebenen Gründen müssen wir auch die Methode hinzufügen . Wenn Ihnen die Leistung nicht so viel bedeutet, gibt es Lösungen für das Schreiben von Besuchern, ohne die zu benötigen.accept()
Diese beinhalten jedoch normalerweise Reflexionen und können daher einen ziemlich hohen Overhead verursachen.quelle
accept()
Methode wird erforderlich, wenn diese Warnung im Besucher verletzt wird.Das wäre natürlich albern, wenn das das einzige wäre so Accept implementiert würde.
Aber es ist nicht.
Zum Beispiel sind Besucher wirklich sehr nützlich, wenn es um Hierarchien geht. In diesem Fall könnte die Implementierung eines nicht-terminalen Knotens so aussehen
Siehst du? Was Sie als dumm beschreiben, ist die Lösung für das Durchqueren von Hierarchien.
Hier ist ein viel längerer und ausführlicher Artikel, der mir den Besucher verständlich gemacht hat .
Bearbeiten: Zur Verdeutlichung: Die Besuchermethode
Visit
enthält Logik, die auf einen Knoten angewendet werden soll. DieAccept
Methode des Knotens enthält eine Logik zum Navigieren zu benachbarten Knoten. Der Fall, wo Sie nur doppelt versenden, ist ein Sonderfall, in dem einfach keine benachbarten Knoten zum Navigieren vorhanden sind.quelle
Der Zweck des Besuchermusters besteht darin, sicherzustellen, dass Objekte wissen, wann der Besucher mit ihnen fertig ist und abgereist ist, damit die Klassen anschließend alle erforderlichen Aufräumarbeiten durchführen können. Außerdem können Klassen ihre Interna "vorübergehend" als "ref" -Parameter verfügbar machen und wissen, dass die Interna nicht mehr verfügbar sind, sobald der Besucher weg ist. In Fällen, in denen keine Bereinigung erforderlich ist, ist das Besuchermuster nicht besonders nützlich. Klassen, die keines dieser Dinge tun, profitieren möglicherweise nicht vom Besuchermuster, aber Code, der zur Verwendung des Besuchermusters geschrieben wurde, kann mit zukünftigen Klassen verwendet werden, die nach dem Zugriff möglicherweise bereinigt werden müssen.
Angenommen, eine Datenstruktur enthält viele Zeichenfolgen, die atomar aktualisiert werden sollen, aber die Klasse, die die Datenstruktur enthält, weiß nicht genau, welche Arten von atomaren Aktualisierungen durchgeführt werden sollen (z. B. wenn ein Thread alle Vorkommen von "ersetzen möchte). X ", während ein anderer Thread eine beliebige Ziffernfolge durch eine numerisch höhere Folge ersetzen möchte, sollten die Operationen beider Threads erfolgreich sein. Wenn jeder Thread einfach eine Zeichenfolge vorliest, seine Aktualisierungen durchführt und sie zurückschreibt, den zweiten Thread das Zurückschreiben seiner Zeichenfolge würde die erste überschreiben). Eine Möglichkeit, dies zu erreichen, besteht darin, dass jeder Thread eine Sperre erhält, seine Operation ausführt und die Sperre aufhebt. Wenn Schlösser auf diese Weise freigelegt werden,
Das Besuchermuster bietet (mindestens) drei Ansätze, um dieses Problem zu vermeiden:
Ohne das Besuchermuster würde das Ausführen atomarer Aktualisierungen das Aufdecken von Sperren und das Risiko eines Fehlers erfordern, wenn die aufrufende Software nicht einem strengen Sperr- / Entsperrprotokoll folgt. Mit dem Besuchermuster können atomare Aktualisierungen relativ sicher durchgeführt werden.
quelle
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. Kunden 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.
Besucher sind nützlich, wenn Sie einer stabilen Klassenfamilie häufig Funktionen hinzufügen, ändern oder entfernen, da jedes Funktionselement in jeder Besucherklasse separat definiert wird und die Klassen selbst nicht geändert werden müssen. Wenn die Klassenfamilie nicht stabil ist, ist das Besuchermuster möglicherweise weniger nützlich, da viele Besucher jedes Mal geändert werden müssen, wenn eine Klasse hinzugefügt oder entfernt wird.
quelle
Ein gutes Beispiel ist die Quellcode-Kompilierung:
Kunden können ein implementieren
JavaBuilder
,RubyBuilder
,XMLValidator
usw. und die Umsetzung für das Sammeln und den Besuch der alle Quelldateien in einem Projekt nicht ändern müssen.Dies wäre ein schlechtes Muster, wenn Sie für jeden Quelldateityp separate Klassen haben:
Es kommt auf den Kontext an und darauf, welche Teile des Systems erweiterbar sein sollen.
quelle