La doppia spedizione è solo una delle ragioni per utilizzare questo modello .
Si noti tuttavia che è l'unico modo per implementare il doppio o più invio in linguaggi che utilizzano un unico paradigma di invio.
Ecco alcuni motivi per utilizzare il modello:
1) Vogliamo definire nuove operazioni senza cambiare il modello ogni volta perché il modello non cambia spesso mentre le operazioni cambiano frequentemente.
2) Non vogliamo abbinare modello e comportamento perché vogliamo avere un modello riutilizzabile in più applicazioni o vogliamo avere un modello estensibile che consenta alle classi client di definire i loro comportamenti con le proprie classi.
3) Abbiamo operazioni comuni che dipendono dal tipo concreto del modello ma non vogliamo implementare la logica in ciascuna sottoclasse in quanto esploderà la logica comune in più classi e quindi in più posizioni .
4) Stiamo usando un modello di progettazione del dominio e le classi di modello della stessa gerarchia eseguono troppe cose distinte che potrebbero essere raccolte altrove .
5) Abbiamo bisogno di una doppia spedizione .
Abbiamo variabili dichiarate con tipi di interfaccia e vogliamo essere in grado di elaborarle in base al loro tipo di runtime ... ovviamente senza usare if (myObj instanceof Foo) {}
o alcun trucco.
L'idea è, ad esempio, di passare queste variabili a metodi che dichiarano un tipo concreto di interfaccia come parametro per applicare un'elaborazione specifica. Questo modo di fare non è possibile out of the box con le lingue si basa su un singolo invio perché il richiamo scelto in fase di runtime dipende solo dal tipo di runtime del ricevitore.
Si noti che in Java, il metodo (firma) da chiamare viene scelto al momento della compilazione e dipende dal tipo dichiarato di parametri, non dal tipo di runtime.
L'ultimo punto che è un motivo per utilizzare il visitatore è anche una conseguenza perché quando si implementa il visitatore (ovviamente per le lingue che non supportano l'invio multiplo), è necessario introdurre un'implementazione a doppio invio.
Si noti che l'attraversamento di elementi (iterazione) per applicare il visitatore su ognuno di essi non è un motivo per utilizzare il modello.
Si utilizza il modello perché è stato diviso il modello e l'elaborazione.
E usando il modello, beneficerai inoltre di un'abilità iteratore.
Questa capacità è molto potente e va oltre l'iterazione sul tipo comune con un metodo specifico come accept()
un metodo generico.
È un caso d'uso speciale. Quindi lo metterò da parte.
Esempio in Java
Illustrerò il valore aggiunto del modello con un esempio di scacchi in cui vorremmo definire l'elaborazione mentre il giocatore richiede un pezzo in movimento.
Senza l'uso del modello visitatore, potremmo definire comportamenti di spostamento dei pezzi direttamente nelle sottoclassi dei pezzi.
Potremmo avere ad esempio Piece
un'interfaccia come:
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
Ogni sottoclasse di Piece lo implementerebbe come:
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
E la stessa cosa per tutte le sottoclassi di Piece.
Ecco una classe di diagramma che illustra questo disegno:
Questo approccio presenta tre importanti inconvenienti:
- comportamenti come performMove()
o computeIfKingCheck()
molto probabilmente useranno una logica comune.
Ad esempio, qualunque sia il calcestruzzo Piece
,performMove()
imposterà finalmente il pezzo corrente in una posizione specifica e potenzialmente prenderà il pezzo avversario.
Dividere i comportamenti correlati in più classi invece di raccoglierli sconfigge in qualche modo il singolo modello di responsabilità. Rendere più difficile la loro manutenibilità.
- l'elaborazione come checkMoveValidity()
non dovrebbe essere qualcosa che le Piece
sottoclassi possono vedere o cambiare.
È un controllo che va oltre le azioni umane o informatiche. Questo controllo viene eseguito ad ogni azione richiesta da un giocatore per assicurarsi che la mossa richiesta sia valida.
Quindi non vogliamo nemmeno fornirlo Piece
nell'interfaccia.
- Nei giochi di scacchi stimolanti per gli sviluppatori di bot, in genere l'applicazione fornisce un'API standard ( Piece
interfacce, sottoclassi, scheda, comportamenti comuni, ecc ...) e consente agli sviluppatori di arricchire la propria strategia di bot.
Per poterlo fare, dobbiamo proporre un modello in cui dati e comportamenti non siano strettamente accoppiati nelle Piece
implementazioni.
Quindi andiamo ad usare il modello visitatore!
Abbiamo due tipi di struttura:
- le classi modello che accettano di essere visitate (i pezzi)
- i visitatori che li visitano (operazioni di trasloco)
Ecco un diagramma di classe che illustra il modello:
Nella parte superiore abbiamo i visitatori e nella parte inferiore abbiamo le classi del modello.
Ecco l' PieceMovingVisitor
interfaccia (comportamento specificato per ogni tipo di 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);
}
Il pezzo è ora definito:
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
Il suo metodo chiave è:
void accept(PieceMovingVisitor pieceVisitor);
Fornisce il primo invio: un'invocazione basata sul Piece
destinatario.
In fase di compilazione, il metodo è associato al accept()
metodo dell'interfaccia di Piece e in fase di esecuzione, il metodo limitato verrà richiamato sulla Piece
classe di runtime .
Ed è ilaccept()
implementazione metodo che eseguirà una seconda spedizione.
In effetti, ogni Piece
sottoclasse che vuole essere visitata da un PieceMovingVisitor
oggetto invoca il PieceMovingVisitor.visit()
metodo passando come argomento stesso.
In questo modo, il compilatore limita non appena il tempo di compilazione, il tipo del parametro dichiarato con il tipo concreto.
C'è la seconda spedizione.
Ecco la Bishop
sottoclasse che illustra che:
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;
}
}
E qui un esempio di utilizzo:
// 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));
}
Svantaggi del visitatore
Il modello Visitatore è un modello molto potente, ma presenta anche alcune importanti limitazioni da considerare prima di utilizzarlo.
1) Rischio di ridurre / rompere l'incapsulamento
In alcuni tipi di operazioni, il modello visitatore può ridurre o interrompere l'incapsulamento di oggetti di dominio.
Ad esempio, poiché la MovePerformingVisitor
classe deve impostare le coordinate del pezzo reale, l' Piece
interfaccia deve fornire un modo per farlo:
void setCoordinates(Coordinates coordinates);
La responsabilità delle Piece
modifiche alle coordinate è ora aperta ad altre classi oltre alle Piece
sottoclassi.
Anche lo spostamento dell'elaborazione eseguita dal visitatore nelle Piece
sottoclassi non è un'opzione.
Creerà sicuramente un altro problema poiché Piece.accept()
accetta qualsiasi implementazione dei visitatori. Non sa cosa esegue il visitatore e quindi non ha idea di se e come cambiare lo stato di Piece.
Un modo per identificare il visitatore sarebbe quello di eseguire una post elaborazione Piece.accept()
secondo l'implementazione del visitatore. Sarebbe una pessima idea in quanto creerebbe un elevato accoppiamento tra le implementazioni dei visitatori e le sottoclassi di Piece e inoltre richiederebbe probabilmente un trucco come getClass()
,instanceof
o qualsiasi indicatore che identifichi l'implementazione dei visitatori.
2) Requisito per modificare il modello
Contrariamente ad altri modelli di progettazione comportamentale come Decorator
ad esempio, il modello di visitatore è invadente.
Dobbiamo davvero modificare la classe iniziale del ricevitore per fornire un accept()
metodo per accettare di essere visitati.
Non abbiamo avuto alcun problema per Piece
e sue sottoclassi in quanto queste sono le nostre classi .
Nelle classi integrate o di terze parti, le cose non sono così facili.
Abbiamo bisogno di avvolgerli o ereditarli (se possibile) per aggiungere il accept()
metodo.
3) Indiretti
Il modello crea più riferimenti indiretti.
La doppia spedizione significa due invocazioni anziché una sola:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
E potremmo avere ulteriori indicazioni indirette quando il visitatore cambia lo stato dell'oggetto visitato.
Può sembrare un ciclo:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)