Doppelter Versand ist nur ein Grund unter anderen, dieses Muster zu verwenden .
Es ist jedoch zu beachten, dass dies der einzige Weg ist, um Double- oder More-Dispatch in Sprachen zu implementieren, die ein Single-Dispatch-Paradigma verwenden.
Hier sind Gründe, das Muster zu verwenden:
1) Wir wollen neue Operationen definieren, ohne das Modell jedes Mal zu ändern weil sich das Modell nicht oft ändert, während sich die Vorgänge häufig ändern.
2) Wir wollen Modell und Verhalten nicht koppeln denn wir wollen ein wiederverwendbares Modell haben in mehreren Anwendungen oder wir wollen ein erweiterbares Modell haben die es Client-Klassen ermöglichen, ihr Verhalten mit ihren eigenen Klassen zu definieren.
3) Wir haben gemeinsame Operationen, die vom konkreten Typ des Modells abhängen, aber wir wollen die Logik nicht in jeder Unterklasse implementieren, da dies die gemeinsame Logik in mehreren Klassen und somit an mehreren Stellen auflösen würde .
4) Wir verwenden einen Domänenmodellentwurf und Modellklassen derselben Hierarchie führen zu viele verschiedene Aufgaben aus, die an anderer Stelle zusammengefasst werden könnten .
5) Wir brauchen einen doppelten Versand .
Wir haben Variablen, die mit Schnittstellentypen deklariert sind, und wir wollen sie entsprechend ihres Laufzeittyps verarbeiten können natürlich ohne die Verwendung von if (myObj instanceof Foo) {}
oder irgendeinen Trick.
Die Idee ist zum Beispiel, diese Variablen an Methoden zu übergeben, die einen konkreten Typ der Schnittstelle als Parameter deklarieren, um eine bestimmte Verarbeitung anzuwenden. Diese Vorgehensweise ist bei Sprachen, die auf einem Single-Dispatch beruhen, nicht ohne weiteres möglich, da der gewählte Aufruf zur Laufzeit nur vom Laufzeittyp des Empfängers abhängt.
Beachten Sie, dass in Java die aufzurufende Methode (Signatur) zur Kompilierzeit ausgewählt wird und vom deklarierten Typ der Parameter abhängt, nicht von ihrem Laufzeittyp.
Der letzte Punkt, der ein Grund für die Verwendung des Besuchers ist, ist auch eine Konsequenz, denn wenn Sie den Besucher implementieren (natürlich für Sprachen, die keine Mehrfachabfertigung unterstützen), müssen Sie zwangsläufig eine Implementierung für die Doppelabfertigung einführen.
Beachten Sie, dass das Durchlaufen der Elemente (Iteration), um den Besucher auf jedes einzelne anzuwenden, kein Grund ist, das Muster zu verwenden.
Sie verwenden das Muster, weil Sie Modell und Verarbeitung trennen.
Und durch die Verwendung des Musters profitieren Sie zusätzlich von einer Iterator-Fähigkeit.
Diese Fähigkeit ist sehr mächtig und geht über die Iteration eines allgemeinen Typs mit einer bestimmten Methode wie accept()
ist eine generische Methode.
Dies ist ein spezieller Anwendungsfall. Ich werde das also beiseite lassen.
Beispiel in Java
Ich werde den Mehrwert des Musters anhand eines Schachbeispiels veranschaulichen, bei dem wir die Verarbeitung definieren möchten, wenn ein Spieler eine Figur bewegen möchte.
Ohne die Verwendung des Besuchermusters könnten wir das Verhalten beim Verschieben von Teilen direkt in den Unterklassen der Teile definieren.
Wir könnten zum Beispiel eine Piece
Schnittstelle wie z. B. :
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
Jede Unterklasse von Piece würde sie implementieren, wie z.B. :
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
Und das Gleiche gilt für alle Unterklassen von Piece.
Das folgende Diagramm veranschaulicht diesen Entwurf:
Dieser Ansatz hat drei wesentliche Nachteile:
- Verhaltensweisen wie performMove()
o computeIfKingCheck()
wird höchstwahrscheinlich die allgemeine Logik anwenden.
Zum Beispiel, was auch immer der konkrete Piece
, performMove()
setzt schließlich die aktuelle Figur an einen bestimmten Ort und nimmt möglicherweise die gegnerische Figur.
Die Aufteilung verwandter Verhaltensweisen in mehrere Klassen, anstatt sie zusammenzufassen, untergräbt in gewisser Weise das Muster der einzigen Verantwortung. Das macht ihre Wartbarkeit schwieriger.
- Verarbeitung als checkMoveValidity()
sollte nicht etwas sein, das die Piece
Unterklassen sehen oder ändern können.
Es handelt sich um eine Kontrolle, die über das Handeln von Menschen oder Computern hinausgeht. Diese Prüfung wird bei jeder von einem Spieler angeforderten Aktion durchgeführt, um sicherzustellen, dass der angeforderte Figurenzug gültig ist.
Wir wollen also nicht einmal das in der Piece
Schnittstelle.
- Bei Schachspielen, die für Bot-Entwickler eine Herausforderung darstellen, bietet die Anwendung im Allgemeinen eine Standard-API ( Piece
Schnittstellen, Unterklassen, Board, gemeinsame Verhaltensweisen usw.) und lassen Entwickler ihre Bot-Strategie bereichern.
Um dies zu erreichen, müssen wir ein Modell vorschlagen, bei dem Daten und Verhaltensweisen nicht eng miteinander gekoppelt sind. Piece
Implementierungen.
Verwenden wir also das Besuchermuster!
Wir haben zwei Arten von Strukturen:
- die Modellklassen, die besucht werden dürfen (die Stücke)
- die Besucher, die sie aufsuchen (bewegliche Vorgänge)
Hier ist ein Klassendiagramm, das das Muster veranschaulicht:
Im oberen Teil haben wir die Besucher und im unteren Teil die Modellklassen.
Hier ist die PieceMovingVisitor
Schnittstelle (das Verhalten ist für jede Art von Piece
) :
public interface PieceMovingVisitor {
void visitPawn(Pawn pawn);
void visitKing(King king);
void visitQueen(Queen queen);
void visitKnight(Knight knight);
void visitRook(Rook rook);
void visitBishop(Bishop bishop);
}
Das Stück ist jetzt definiert:
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
Seine wichtigste Methode ist :
void accept(PieceMovingVisitor pieceVisitor);
Es bietet den ersten Versand: einen Aufruf auf der Grundlage der Piece
Empfänger.
Zur Kompilierungszeit ist die Methode an die accept()
Methode der Schnittstelle Piece und zur Laufzeit wird die Methode Bounded auf der Laufzeit Piece
Klasse.
Und es ist die accept()
Methodenimplementierung, die einen zweiten Versand durchführen wird.
In der Tat, jeder Piece
Unterklasse, die besucht werden möchte von einer PieceMovingVisitor
Objekt ruft die PieceMovingVisitor.visit()
Methode, indem sie selbst als Argument übergeben wird.
Auf diese Weise grenzt der Compiler bereits zur Kompilierzeit den Typ des deklarierten Parameters mit dem konkreten Typ ab.
Es gibt die zweite Sendung.
Hier ist die Bishop
Unterklasse, die veranschaulicht, dass :
public class Bishop implements Piece {
private Coordinates coord;
public Bishop(Coordinates coord) {
super(coord);
}
@Override
public void accept(PieceMovingVisitor pieceVisitor) {
pieceVisitor.visitBishop(this);
}
@Override
public Coordinates getCoordinates() {
return coordinates;
}
@Override
public void setCoordinates(Coordinates coordinates) {
this.coordinates = coordinates;
}
}
Und hier ein Anwendungsbeispiel:
// 1. Player requests a move for a specific piece
Piece piece = selectPiece();
Coordinates coord = selectCoordinates();
// 2. We check with MoveCheckingVisitor that the request is valid
final MoveCheckingVisitor moveCheckingVisitor = new MoveCheckingVisitor(coord);
piece.accept(moveCheckingVisitor);
// 3. If the move is valid, MovePerformingVisitor performs the move
if (moveCheckingVisitor.isValid()) {
piece.accept(new MovePerformingVisitor(coord));
}
Nachteile für Besucher
Das Visitor-Muster ist ein sehr leistungsfähiges Muster, hat aber auch einige wichtige Einschränkungen, die Sie berücksichtigen sollten, bevor Sie es verwenden.
1) Risiko der Verringerung/Brechung der Verkapselung
Bei einigen Betriebsarten kann das Besuchermuster die Kapselung von Domänenobjekten verringern oder aufheben.
Zum Beispiel, als die MovePerformingVisitor
Klasse muss die Koordinaten des eigentlichen Stücks festlegen, die Piece
Schnittstelle muss eine Möglichkeit bieten, dies zu tun:
void setCoordinates(Coordinates coordinates);
Die Verantwortung der Piece
Koordinatenänderungen sind nun auch für andere Klassen als Piece
Unterklassen.
Verschieben der Verarbeitung durch den Besucher im Piece
Unterklassen ist ebenfalls keine Option.
Dies wird in der Tat ein weiteres Problem darstellen, da die Piece.accept()
akzeptiert jede Besucherimplementierung. Es weiß nicht, was der Besucher tut und hat daher keine Ahnung, ob und wie es den Zustand des Stücks ändern kann.
Eine Möglichkeit, den Besucher zu identifizieren, wäre eine Nachbearbeitung in Piece.accept()
je nach der Implementierung des Besuchers. Es wäre eine sehr schlechte Idee, da es eine hohe Kopplung zwischen Visitor-Implementierungen und Piece-Unterklassen schaffen würde und außerdem würde es wahrscheinlich erfordern, Trick als getClass()
, instanceof
oder eine beliebige Markierung, die die Implementierung des Besuchers kennzeichnet.
2) Erfordernis, das Modell zu ändern
Im Gegensatz zu einigen anderen verhaltensorientierten Entwurfsmustern wie Decorator
zum Beispiel ist das Besuchermuster aufdringlich.
Wir müssen in der Tat die ursprüngliche Empfängerklasse ändern, um eine accept()
Methode zu akzeptieren, um besucht zu werden.
Wir hatten keine Probleme mit der Piece
und ihre Unterklassen, da diese unsere Klassen .
Bei integrierten Klassen oder Klassen von Drittanbietern sind die Dinge nicht so einfach.
Wir müssen sie einpacken oder vererben (wenn wir können), um die accept()
Methode.
3) Indirektionen
Das Muster erzeugt mehrfache Umwege.
Die doppelte Versendung bedeutet zwei Aufrufe anstelle eines einzigen Aufrufs:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
Und wir könnten zusätzliche Umleitungen haben, wenn der Besucher den Zustand des besuchten Objekts ändert.
Es mag wie ein Zyklus aussehen:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)