Dr. Yan Hongs Buch „JAVA and Patterns“ beginnt mit einer Beschreibung des Besuchermusters:
Unter Besuchermuster versteht man das Verhaltensmuster von Objekten. Der Zweck des Besuchermusters besteht darin, einige Vorgänge zu kapseln, die auf bestimmte Datenstrukturelemente angewendet werden. Sobald diese Operationen geändert werden müssen, kann die Datenstruktur, die diese Operation akzeptiert, unverändert bleiben.
Das Konzept des Versands
Der Typ, wenn eine Variable deklariert wird, wird als statischer Typ der Variablen (statischer Typ) bezeichnet, und manche Leute nennen den statischen Typ den scheinbaren Typ (scheinbarer Typ); tatsächlicher Typ der Variablen (Actual Type). Zum Beispiel:
Kopieren Sie den Codecode wie folgt:
Listenliste = null;
list = new ArrayList();
Eine Variablenliste wird deklariert, ihr statischer Typ (auch offensichtlicher Typ genannt) ist List und ihr tatsächlicher Typ ist ArrayList.
Die Auswahl der Methoden basiert auf dem Objekttyp. Der Versand wird in zwei Typen unterteilt, nämlich den statischen Versand und den dynamischen Versand.
Der statische Versand erfolgt zur Kompilierungszeit und der Versand erfolgt basierend auf statischen Typinformationen. Statischer Versand ist für uns kein Unbekannter. Methodenüberladung ist statischer Versand.
Der dynamische Versand erfolgt zur Laufzeit und der dynamische Versand ersetzt eine Methode dynamisch.
statischer Versand
Java unterstützt den statischen Versand durch Methodenüberladung. Am Beispiel der Geschichte, in der Mozi auf einem Pferd reitet, könnte Mozi ein weißes oder ein schwarzes Pferd reiten. Das Klassendiagramm von Mozi und weißem Pferd, schwarzem Pferd und Pferd sieht wie folgt aus:
In diesem System wird Mozi durch die Mozi-Klasse dargestellt. Der Code lautet wie folgt:
öffentliche Klasse Mozi {
Öffentlicher Leerritt (Pferd h){
System.out.println("Reiten");
}
Öffentlicher leerer Ritt (WhiteHorse wh){
System.out.println("auf einem weißen Pferd reiten");
}
Fahrt in der öffentlichen Leere (BlackHorse bh){
System.out.println("Reite auf dem dunklen Pferd");
}
public static void main(String[] args) {
Pferd wh = new WhiteHorse();
Pferd bh = new BlackHorse();
Mozi mozi = new Mozi();
mozi.ride(wh);
mozi.ride(bh);
}
}
Offensichtlich ist die Ride()-Methode der Mozi-Klasse von drei Methoden überladen. Diese drei Methoden akzeptieren Parameter vom Typ Horse, WhiteHorse, BlackHorse und andere Typen.
Welche Ergebnisse wird das Programm also ausgeben, wenn es ausgeführt wird? Das Ergebnis ist, dass das Programm die gleichen zwei Zeilen „Horseback“ druckt. Mit anderen Worten: Mozi entdeckte, dass er nur auf Pferden ritt.
Warum? Die beiden Aufrufe der Methode ride() übergeben unterschiedliche Parameter, nämlich wh und bh. Obwohl sie unterschiedliche reale Typen haben, sind ihre statischen Typen alle gleich, nämlich Pferdetypen.
Der Versand überladener Methoden basiert auf statischen Typen und dieser Versandvorgang wird zur Kompilierungszeit abgeschlossen.
dynamischer Versand
Java unterstützt den dynamischen Versand durch Methodenüberschreibung. Am Beispiel der Geschichte eines Pferdes, das Gras frisst, lautet der Code wie folgt:
Kopieren Sie den Codecode wie folgt:
öffentliche Klasse Pferd {
public void eat(){
System.out.println("Pferd frisst Gras");
}
}
Kopieren Sie den Codecode wie folgt:
öffentliche Klasse BlackHorse erweitert Horse {
@Override
public void eat() {
System.out.println("Dunkles Pferd frisst Gras");
}
}
Kopieren Sie den Codecode wie folgt:
öffentliche Klasse Client {
public static void main(String[] args) {
Pferd h = new BlackHorse();
Hitze();
}
}
Der statische Typ der Variablen h ist Horse und der reale Typ ist BlackHorse. Wenn die eat()-Methode in der letzten Zeile oben die eat()-Methode der BlackHorse-Klasse aufruft, wird oben „Black Horse Eating Grass“ ausgegeben, wenn die eat()-Methode oben die eat( )-Methode der Horse-Klasse, dann wird „Pferd frisst Gras“ ausgegeben.
Der Kern des Problems besteht daher darin, dass der Java-Compiler nicht immer weiß, welcher Code während der Kompilierung ausgeführt wird, da der Compiler nur den statischen Typ des Objekts kennt, aber nicht den tatsächlichen Typ des Objekts und der Methode Der Aufruf basiert auf den Real-Typen des Objekts, nicht auf statischen Typen. Auf diese Weise ruft die eat()-Methode in der letzten Zeile oben die eat()-Methode der BlackHorse-Klasse auf und gibt „schwarzes Pferd frisst Gras“ aus.
Versandart
Das Objekt, zu dem eine Methode gehört, wird als Empfänger der Methode bezeichnet. Der Empfänger der Methode und die Parameter der Methode werden zusammen als Volumen der Methode bezeichnet. Der Kopiercode der Testklasse im folgenden Beispiel lautet beispielsweise wie folgt:
öffentlicher Klassentest {
public void print(String str){
System.out.println(str);
}
}
In der obigen Klasse gehört die print()-Methode zum Testobjekt, sodass ihr Empfänger auch das Testobjekt ist. Die print()-Methode hat einen Parameter namens str und sein Typ ist String.
Abhängig davon, wie viele Arten von Mengenversand zugrunde gelegt werden können, können objektorientierte Sprachen in Single-Dispatch-Sprachen (Uni-Dispatch) und Multi-Dispatch-Sprachen (Multi-Dispatch) unterteilt werden. Single-Dispatch-Sprachen wählen Methoden basierend auf dem Typ einer Instanz aus, während Multi-Dispatch-Sprachen Methoden basierend auf dem Typ mehrerer Instanzen auswählen.
Sowohl C++ als auch Java sind Single-Dispatch-Sprachen, und Beispiele für Multi-Dispatch-Sprachen sind CLOS und Cecil. Nach dieser Unterscheidung ist Java eine dynamische Single-Dispatch-Sprache, da der dynamische Versand dieser Sprache nur den Typ des Methodenempfängers berücksichtigt, und es handelt sich um eine statische Multi-Dispatch-Sprache, da diese Sprache überladene Methoden versendet Dabei werden der Typ des Empfängers der Methode und die Typen aller Parameter der Methode berücksichtigt.
In einer Sprache, die dynamischen Einzelversand unterstützt, gibt es zwei Bedingungen, die bestimmen, welche Operation eine Anfrage aufruft: eine ist der Name der Anfrage und der tatsächliche Typ des Empfängers. Der einmalige Versand schränkt den Methodenauswahlprozess ein, sodass nur eine Instanz berücksichtigt werden kann, bei der es sich normalerweise um den Empfänger der Methode handelt. Wenn in der Java-Sprache eine Operation an einem Objekt unbekannten Typs ausgeführt wird, erfolgt der tatsächliche Typtest des Objekts nur einmal. Dies ist das Merkmal des dynamischen Einzelversands.
Doppelversand
Eine Methode entscheidet, basierend auf den Typen zweier Variablen unterschiedlichen Code auszuführen. Dies ist ein „doppelter Versand“. Die Java-Sprache unterstützt keinen dynamischen Mehrfachversand, was bedeutet, dass Java keinen dynamischen Doppelversand unterstützt. Durch die Verwendung von Entwurfsmustern kann dynamischer Doppelversand aber auch in der Java-Sprache implementiert werden.
In Java können zwei Dispatches durch zwei Methodenaufrufe erreicht werden. Das Klassendiagramm sieht wie folgt aus:
Auf dem Bild sind zwei Objekte zu sehen, das linke heißt „West“ und das rechte heißt „Ost“. Nun ruft das West-Objekt zunächst die Methode goEast() des Ost-Objekts auf und übergibt sich selbst. Wenn das East-Objekt aufgerufen wird, erkennt es anhand der übergebenen Parameter sofort, wer der Anrufer ist, sodass nacheinander die goWest()-Methode des „caller“-Objekts aufgerufen wird. Durch zwei Aufrufe wird die Programmsteuerung nacheinander an zwei Objekte übergeben. Das Sequenzdiagramm sieht wie folgt aus:
Auf diese Weise gibt es zwei Methodenaufrufe zwischen den beiden Objekten. Zuerst wird sie vom West-Objekt an das Ost-Objekt übergeben und dann wird sie an das West-Objekt zurückgegeben.
Doch allein das Zurückspielen des Balls löst das Problem der Doppelverteilung nicht. Der Schlüssel liegt darin, wie diese beiden Aufrufe und die dynamische Single-Dispatch-Funktion der Java-Sprache verwendet werden, um während dieses Übergabeprozesses zwei Single-Dispatches auszulösen.
Der dynamische Einzelversand in der Java-Sprache erfolgt, wenn eine Unterklasse eine Methode einer übergeordneten Klasse überschreibt. Mit anderen Worten: Sowohl West als auch East müssen in ihre eigene Typhierarchie eingeordnet werden, wie unten gezeigt:
Quellcode
Der Kopiercode der West-Klasse lautet wie folgt:
öffentliche abstrakte Klasse West {
public abstract void goWest1(SubEast1 east);
public abstract void goWest2(SubEast2 east);
}
Der Kopiercode der SubWest1-Klasse lautet wie folgt:
öffentliche Klasse SubWest1 erweitert West{
@Override
public void goWest1(SubEast1 east) {
System.out.println("SubWest1 + " + east.myName1());
}
@Override
public void goWest2(SubEast2 east) {
System.out.println("SubWest1 + " + east.myName2());
}
}
SubWest-Klasse 2
Kopieren Sie den Codecode wie folgt:
öffentliche Klasse SubWest2 erweitert West{
@Override
public void goWest1(SubEast1 east) {
System.out.println("SubWest2 + " + east.myName1());
}
@Override
public void goWest2(SubEast2 east) {
System.out.println("SubWest2 + " + east.myName2());
}
}
Der Kopiercode der East-Klasse lautet wie folgt:
öffentliche abstrakte Klasse Ost {
public abstract void goEast(West west);
}
Der Code zum Kopieren der SubEast1-Klasse lautet wie folgt:
öffentliche Klasse SubEast1 erweitert East{
@Override
öffentliche Leere goEast(West west) {
west.goWest1(this);
}
öffentlicher String myName1(){
return „SubEast1“;
}
}
Der Kopiercode der SubEast2-Klasse lautet wie folgt:
öffentliche Klasse SubEast2 erweitert East{
@Override
öffentliche Leere goEast(West west) {
west.goWest2(this);
}
öffentlicher String myName2(){
return „SubEast2“;
}
}
Der Kopiercode der Client-Klasse lautet wie folgt:
öffentliche Klasse Client {
public static void main(String[] args) {
//Kombination 1
East East = new SubEast1();
West West = new SubWest1();
east.goEast(west);
//Kombination 2
east = new SubEast1();
west = new SubWest2();
east.goEast(west);
}
}
Die laufenden Ergebnisse lauten wie folgt.
SubWest1 + SubEast1
SubWest2 + SubEast1
Wenn das System ausgeführt wird, werden zunächst SubWest1- und SubEast1-Objekte erstellt. Anschließend ruft der Client die goEast()-Methode von SubEast1 auf und übergibt das SubWest1-Objekt. Da das SubEast1-Objekt die goEast()-Methode seiner Oberklasse East überschreibt, erfolgt zu diesem Zeitpunkt ein dynamischer Einzelversand. Wenn das SubEast1-Objekt den Aufruf empfängt, ruft es das SubWest1-Objekt aus dem Parameter ab, ruft also sofort die goWest1()-Methode dieses Objekts auf und übergibt sich selbst. Da das SubEast1-Objekt das Recht hat, auszuwählen, welches Objekt aufgerufen werden soll, wird zu diesem Zeitpunkt ein weiterer dynamischer Methodenversand durchgeführt.
Zu diesem Zeitpunkt hat das SubWest1-Objekt das SubEast1-Objekt erhalten. Durch Aufrufen der Methode myName1() dieses Objekts können Sie Ihren eigenen Namen und den Namen des SubEast-Objekts ausdrucken. Das Sequenzdiagramm lautet wie folgt:
Da einer dieser beiden Namen aus der Osthierarchie und der andere aus der Westhierarchie stammt, wird ihre Kombination dynamisch bestimmt. Dies ist der Implementierungsmechanismus des dynamischen Doppelversands.
Die Struktur des Besuchermusters
Das Besuchermuster eignet sich für Systeme mit relativ unbestimmten Datenstrukturen. Es entkoppelt die Kopplung zwischen der Datenstruktur und den Operationen, die auf die Struktur einwirken, sodass sich die Menge der Operationen relativ frei entwickeln kann. Ein vereinfachtes Diagramm des Besuchermusters ist unten dargestellt:
Jeder Knoten der Datenstruktur kann einen Anruf von einem Besucher annehmen. Dieser Knoten übergibt das Knotenobjekt an das Besucherobjekt, und das Besucherobjekt führt wiederum die Operationen des Knotenobjekts aus. Dieser Vorgang wird als „Doppelversand“ bezeichnet. Der Knoten ruft den Besucher auf, übergibt sich selbst und der Besucher führt einen Algorithmus für diesen Knoten aus. Unten ist ein schematisches Klassendiagramm für das Besuchermuster dargestellt:
Die im Besuchermodus beteiligten Rollen sind wie folgt:
● Abstrakte Besucherrolle (Besucher) : Deklariert eine oder mehrere Methodenoperationen, um die Schnittstelle zu bilden, die alle spezifischen Besucherrollen implementieren müssen.
● Rolle des konkreten Besuchers (ConcreteVisitor) : Implementiert die vom abstrakten Besucher deklarierte Schnittstelle, dh jede vom abstrakten Besucher deklarierte Zugriffsoperation.
● Rolle des abstrakten Knotens (Knoten) : Deklariert eine Akzeptanzoperation und akzeptiert ein Besucherobjekt als Parameter.
● ConcreteNode-Rolle : Implementiert die vom abstrakten Knoten angegebene Akzeptanzoperation.
● Rolle des Strukturobjekts (ObjectStructure) : Hat die folgenden Verantwortlichkeiten, kann bei Bedarf alle Elemente in der Struktur durchlaufen, stellt eine Schnittstelle auf hoher Ebene bereit, sodass Besucherobjekte bei Bedarf auf jedes Element zugreifen können, und kann als zusammengesetztes Objekt entworfen werden Eine Sammlung, z. B. List oder Set.
Quellcode
Wie Sie sehen, bereitet die abstrakte Besucherrolle eine Zugriffsoperation für jeden spezifischen Knoten vor. Da es zwei Knoten gibt, gibt es zwei entsprechende Zugriffsvorgänge.
Kopieren Sie den Codecode wie folgt:
öffentliche Schnittstelle Besucher {
/**
* Entspricht der Zugriffsoperation von NodeA
*/
öffentlicher Void-Besuch (NodeA-Knoten);
/**
* Entspricht der Zugriffsoperation von NodeB
*/
öffentlicher Void-Besuch (NodeB-Knoten);
}
Der spezifische Kopiercode der VisitorA-Klasse lautet wie folgt:
Die öffentliche Klasse VisitorA implementiert Visitor {
/**
* Entspricht der Zugriffsoperation von NodeA
*/
@Override
public void visit(NodeA node) {
System.out.println(node.operationA());
}
/**
* Entspricht der Zugriffsoperation von NodeB
*/
@Override
public void visit(NodeB node) {
System.out.println(node.operationB());
}
}
Der spezifische Kopiercode der VisitorB-Klasse für Besucher lautet wie folgt:
Die öffentliche Klasse VisitorB implementiert Visitor {
/**
* Entspricht der Zugriffsoperation von NodeA
*/
@Override
public void visit(NodeA node) {
System.out.println(node.operationA());
}
/**
* Entspricht der Zugriffsoperation von NodeB
*/
@Override
public void visit(NodeB node) {
System.out.println(node.operationB());
}
}
Der Code zum Kopieren der abstrakten Knotenklasse lautet wie folgt:
öffentliche abstrakte Klasse Node {
/**
* Akzeptieren Sie den Vorgang
*/
public abstract void Accept(Besucher Besucher);
}
Spezifische Knotenklasse NodeA
Kopieren Sie den Codecode wie folgt:
Die öffentliche Klasse NodeA erweitert Node{
/**
* Akzeptieren Sie den Vorgang
*/
@Override
public void Accept(Besucher Besucher) {
besucher.visit(this);
}
/**
*NodeA-spezifische Methode
*/
öffentliche String-OperationA(){
return „NodeA“;
}
}
Spezifische Knotenklasse NodeB
Kopieren Sie den Codecode wie folgt:
öffentliche Klasse NodeB erweitert Node{
/**
*Methode akzeptieren
*/
@Override
public void Accept(Besucher Besucher) {
besucher.visit(this);
}
/**
*NodeB-spezifische Methoden
*/
öffentliche String-OperationB(){
return „NodeB“;
}
}
Strukturelle Objektrollenklasse Diese strukturelle Objektrolle enthält eine Sammlung und stellt der Außenwelt die Methode add() als Verwaltungsoperation für die Sammlung bereit. Durch den Aufruf dieser Methode kann ein neuer Knoten dynamisch hinzugefügt werden.
Kopieren Sie den Codecode wie folgt:
öffentliche Klasse ObjectStructure {
private List<Node> nodes = new ArrayList<Node>();
/**
* Methodenoperation ausführen
*/
öffentliche Nichtigkeitsklage (Besucher Besucher){
for(Knoten Knoten: Knoten)
{
node.accept(visitor);
}
}
/**
* Fügen Sie ein neues Element hinzu
*/
public void add(Knotenknoten){
nodes.add(node);
}
}
Der Kopiercode der Client-Klasse lautet wie folgt:
öffentliche Klasse Client {
public static void main(String[] args) {
//Erstelle ein Strukturobjekt
ObjectStructure os = new ObjectStructure();
//Füge einen Knoten zur Struktur hinzu
os.add(new NodeA());
//Füge einen Knoten zur Struktur hinzu
os.add(new NodeB());
//Erstelle einen Besucher
Besucher Besucher = new VisitorA();
os.action(visitor);
}
}
Obwohl in dieser schematischen Implementierung keine komplexe Objektbaumstruktur mit mehreren Verzweigungsknoten erscheint, wird in tatsächlichen Systemen das Besuchermuster normalerweise zur Behandlung komplexer Objektbaumstrukturen verwendet, und das Besuchermuster kann zur Behandlung von Baumstrukturproblemen verwendet werden, die mehrere Hierarchien umfassen . Hier ist das Besuchermuster so mächtig.
Ablaufdiagramm des Vorbereitungsprozesses
Dieser anschauliche Client erstellt zunächst ein Strukturobjekt und übergibt dann ein neues NodeA-Objekt und ein neues NodeB-Objekt.
Zweitens erstellt der Client ein VisitorA-Objekt und übergibt dieses Objekt an das Strukturobjekt.
Anschließend ruft der Client die Strukturobjekt-Aggregationsverwaltungsmethode auf, um die Knoten NodeA und NodeB zum Strukturobjekt hinzuzufügen.
Abschließend ruft der Client die Aktionsmethode action() des Strukturobjekts auf, um den Zugriffsprozess zu starten.
Zugriff auf das Prozessablaufdiagramm
Das Strukturobjekt durchläuft alle Knoten in der Sammlung, die es speichert. In diesem System handelt es sich um die Knoten NodeA und NodeB. Zunächst wird auf NodeA zugegriffen. Dieser Zugriff besteht aus den folgenden Vorgängen:
(1) Die Methode „accept()“ des NodeA-Objekts wird aufgerufen und das VisitorA-Objekt selbst übergeben.
(2) Das NodeA-Objekt ruft wiederum die Zugriffsmethode des VisitorA-Objekts auf und übergibt das NodeA-Objekt selbst;
(3) Das VisitorA-Objekt ruft die eindeutige Methode operationA() des NodeA-Objekts auf.
Damit ist der Double-Dispatch-Prozess abgeschlossen. Der Zugriffsprozess ist derselbe wie der Zugriffsprozess von NodeA, der hier nicht beschrieben wird.
Vorteile des Besuchermusters
● Durch die gute Erweiterbarkeit können den Elementen in der Objektstruktur neue Funktionen hinzugefügt werden, ohne dass die Elemente in der Objektstruktur geändert werden müssen.
● Eine gute Wiederverwendbarkeit ermöglicht es Besuchern, gemeinsame Funktionen für die gesamte Objektstruktur zu definieren und so den Grad der Wiederverwendbarkeit zu verbessern.
● Trennen irrelevanter Verhaltensweisen Sie können Besucher verwenden, um irrelevante Verhaltensweisen zu trennen und verwandte Verhaltensweisen zusammenzufassen, um einen Besucher zu bilden, sodass die Funktion jedes Besuchers relativ einheitlich ist.
Nachteile des Besuchermusters
● Es ist schwierig, die Objektstruktur zu ändern. Dies ist nicht für Situationen geeignet, in denen sich die Objektstruktur häufig ändert. Daher müssen sich die Benutzeroberfläche und die Implementierung des Besuchers entsprechend ändern.
● Das Durchbrechen des Kapselungs-Besuchermusters erfordert normalerweise, dass die Objektstruktur interne Daten für Besucher und ObjectStructrue öffnet, wodurch die Kapselung des Objekts aufgehoben wird.