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:
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:
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 Piece
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 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 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 aufgerufen Piece
.
Und es ist die accept()
Methodenimplementierung, die einen zweiten Versand durchführt.
In der Tat ruft jede Piece
Unterklasse, die von einem PieceMovingVisitor
Objekt 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 Bishop
Unterklasse, 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 Piece
Schnittstelle eine Möglichkeit bieten, dies zu tun:
void setCoordinates(Coordinates coordinates);
Die Verantwortung für Piece
Koordinatenänderungen steht jetzt anderen Klassen als Piece
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 wie getClass()
, 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:
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)