La double expédition n'est qu'une raison parmi d'autres d'utiliser ce modèle .
Mais notez que c'est la seule façon d'implémenter une répartition double ou plus dans les langues qui utilise un paradigme de répartition unique.
Voici les raisons d'utiliser le modèle:
1) Nous voulons définir de nouvelles opérations sans changer le modèle à chaque fois car le modèle ne change pas souvent alors que les opérations changent fréquemment.
2) Nous ne voulons pas coupler modèle et comportement car nous voulons avoir un modèle réutilisable dans plusieurs applications ou nous voulons avoir un modèle extensible qui permet aux classes clientes de définir leurs comportements avec leurs propres classes.
3) Nous avons des opérations communes qui dépendent du type concret du modèle mais nous ne voulons pas implémenter la logique dans chaque sous-classe car cela exploserait la logique commune dans plusieurs classes et donc à plusieurs endroits .
4) Nous utilisons une conception de modèle de domaine et les classes de modèle de la même hiérarchie effectuent trop de choses distinctes qui pourraient être rassemblées ailleurs .
5) Nous avons besoin d'une double expédition .
Nous avons des variables déclarées avec des types d'interface et nous voulons pouvoir les traiter en fonction de leur type d'exécution… bien sûr sans utiliser if (myObj instanceof Foo) {}
ni truc.
L'idée est par exemple de passer ces variables à des méthodes qui déclarent un type concret de l'interface comme paramètre pour appliquer un traitement spécifique. Cette façon de faire n'est pas possible à la sortie de la boîte avec les langues repose sur une seule expédition car le choix invoqué au moment de l'exécution dépend uniquement du type d'exécution du récepteur.
Notez qu'en Java, la méthode (signature) à appeler est choisie au moment de la compilation et dépend du type déclaré des paramètres, pas de leur type d'exécution.
Le dernier point qui est une raison d'utiliser le visiteur est également une conséquence car lorsque vous implémentez le visiteur (bien sûr pour les langues qui ne prennent pas en charge la répartition multiple), vous devez nécessairement introduire une implémentation à double répartition.
Notez que la traversée d'éléments (itération) pour appliquer le visiteur sur chacun d'eux n'est pas une raison pour utiliser le motif.
Vous utilisez le modèle car vous divisez le modèle et le traitement.
Et en utilisant le modèle, vous bénéficiez en plus d'une capacité d'itérateur.
Cette capacité est très puissante et va au-delà de l'itération sur un type commun avec une méthode spécifique comme accept()
c'est une méthode générique.
Il s'agit d'un cas d'utilisation spécial. Je vais donc mettre cela de côté.
Exemple en Java
Je vais illustrer la valeur ajoutée du motif avec un exemple d'échecs où nous aimerions définir le traitement lorsque le joueur demande le déplacement d'une pièce.
Sans l'utilisation du modèle de visiteur, nous pourrions définir des comportements de déplacement de pièces directement dans les sous-classes de pièces.
On pourrait avoir par exemple une Piece
interface telle que:
public interface Piece{
boolean checkMoveValidity(Coordinates coord);
void performMove(Coordinates coord);
Piece computeIfKingCheck();
}
Chaque sous-classe Piece le mettrait en œuvre, par exemple:
public class Pawn implements Piece{
@Override
public boolean checkMoveValidity(Coordinates coord) {
...
}
@Override
public void performMove(Coordinates coord) {
...
}
@Override
public Piece computeIfKingCheck() {
...
}
}
Et la même chose pour toutes les sous-classes Piece.
Voici une classe de diagramme qui illustre cette conception:
Cette approche présente trois inconvénients importants:
- des comportements tels que performMove()
ou computeIfKingCheck()
utiliseront très probablement une logique commune.
Par exemple, quel que soit le béton Piece
, performMove()
définira finalement la pièce actuelle à un emplacement spécifique et prendra éventuellement la pièce adverse.
Diviser les comportements associés en plusieurs classes au lieu de les rassembler détruit en quelque sorte le modèle de responsabilité unique. Rendre leur maintenabilité plus difficile.
- le traitement checkMoveValidity()
ne devrait pas être quelque chose que les Piece
sous-classes peuvent voir ou changer.
C'est un contrôle qui va au-delà des actions humaines ou informatiques. Cette vérification est effectuée à chaque action demandée par un joueur pour s'assurer que le mouvement de pièce demandé est valide.
Donc, nous ne voulons même pas fournir cela dans l' Piece
interface.
- Dans les jeux d'échecs difficiles pour les développeurs de robots, l'application fournit généralement une API standard ( Piece
interfaces, sous-classes, Board, comportements communs, etc.) et permet aux développeurs d'enrichir leur stratégie de robots.
Pour ce faire, nous devons proposer un modèle où les données et les comportements ne sont pas étroitement couplés dans les Piece
implémentations.
Alors allons-y pour utiliser le modèle de visiteur!
Nous avons deux types de structure:
- les classes modèles qui acceptent d'être visitées (les pièces)
- les visiteurs qui les visitent (opérations de déménagement)
Voici un diagramme de classe qui illustre le modèle:
Dans la partie supérieure, nous avons les visiteurs et dans la partie inférieure, nous avons les classes modèles.
Voici l' PieceMovingVisitor
interface (comportement spécifié pour chaque type de 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);
}
La pièce est définie maintenant:
public interface Piece {
void accept(PieceMovingVisitor pieceVisitor);
Coordinates getCoordinates();
void setCoordinates(Coordinates coordinates);
}
Sa méthode clé est:
void accept(PieceMovingVisitor pieceVisitor);
Il fournit le premier envoi: une invocation basée sur le Piece
récepteur.
Au moment de la compilation, la méthode est liée à la accept()
méthode de l'interface Piece et au moment de l'exécution, la méthode limitée sera invoquée sur la Piece
classe d' exécution .
Et c'est l' accept()
implémentation de la méthode qui effectuera une deuxième répartition.
En effet, chaque Piece
sous-classe qui veut être visitée par un PieceMovingVisitor
objet invoque la PieceMovingVisitor.visit()
méthode en passant comme argument lui-même.
De cette façon, le compilateur délimite dès la compilation, le type du paramètre déclaré avec le type concret.
Il y a la deuxième dépêche.
Voici la Bishop
sous-classe qui illustre cela:
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;
}
}
Et voici un exemple d'utilisation:
// 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));
}
Inconvénients pour les visiteurs
Le modèle de visiteur est un modèle très puissant, mais il a également quelques limitations importantes que vous devez considérer avant de l'utiliser.
1) Risque de réduire / casser l'encapsulation
Dans certains types d'opérations, le modèle de visiteur peut réduire ou interrompre l'encapsulation des objets de domaine.
Par exemple, comme la MovePerformingVisitor
classe doit définir les coordonnées de la pièce réelle, l' Piece
interface doit fournir un moyen de le faire:
void setCoordinates(Coordinates coordinates);
La responsabilité des Piece
changements de coordonnées est désormais ouverte à d'autres classes que les Piece
sous-classes.
Déplacer le traitement effectué par le visiteur dans les Piece
sous-classes n'est pas non plus une option.
Cela créera en effet un autre problème car le Piece.accept()
accepte toute implémentation de visiteur. Il ne sait pas ce que le visiteur effectue et donc aucune idée de savoir si et comment changer l'état de la pièce.
Un moyen d'identifier le visiteur serait d'effectuer un post-traitement en Piece.accept()
fonction de l'implémentation du visiteur. Ce serait une très mauvaise idée car cela créerait un fort couplage entre les implémentations des visiteurs et des sous - classes Piece et d' ailleurs , il aurait probablement besoin d'utiliser comme truc getClass()
, instanceof
ou tout autre marqueur identifiant la mise en œuvre des visiteurs.
2) Obligation de changer de modèle
Contrairement à certains autres modèles de conception comportementale comme Decorator
par exemple, le modèle de visiteur est intrusif.
Il nous faut en effet modifier la classe réceptrice initiale pour fournir une accept()
méthode à accepter d'être visitée.
Nous n'avons eu aucun problème pour Piece
et ses sous-classes car ce sont nos classes .
Dans les classes intégrées ou tierces, les choses ne sont pas si faciles.
Nous devons les envelopper ou les hériter (si nous le pouvons) pour ajouter la accept()
méthode.
3) Indirections
Le motif crée de multiples indirections.
La double répartition signifie deux invocations au lieu d'une seule:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor)
Et nous pourrions avoir des indirections supplémentaires lorsque le visiteur modifie l'état de l'objet visité.
Cela peut ressembler à un cycle:
call the visited (piece) -> that calls the visitor (pieceMovingVisitor) -> that calls the visited (piece)