Les structures visit
/ du modèle de visiteur accept
sont un mal nécessaire en raison de la sémantique des langages de type C (C #, Java, etc.). L'objectif du modèle de visiteur est d'utiliser la double répartition pour acheminer votre appel comme vous vous attendez à la lecture du code.
Normalement, lorsque le modèle de visiteur est utilisé, une hiérarchie d'objets est impliquée où tous les nœuds sont dérivés d'un Node
type de base , désormais appelé Node
. Instinctivement, nous l'écririons comme ceci:
Node root = GetTreeRoot();
new MyVisitor().visit(root);
C'est ici que se trouve le problème. Si notre MyVisitor
classe était définie comme suit:
class MyVisitor implements IVisitor {
void visit(CarNode node);
void visit(TrainNode node);
void visit(PlaneNode node);
void visit(Node node);
}
Si, au moment de l'exécution, quel que soit le type réelroot
, notre appel irait dans la surcharge visit(Node node)
. Cela serait vrai pour toutes les variables déclarées de type Node
. Pourquoi est-ce? Parce que Java et d'autres langages de type C ne prennent en compte que le type statique , ou le type sous lequel la variable est déclarée, du paramètre lors du choix de la surcharge à appeler. Java ne fait pas l'étape supplémentaire pour demander, pour chaque appel de méthode, à l'exécution, "D'accord, quel est le type dynamique de root
? Oh, je vois. C'est un TrainNode
. Voyons s'il y a une méthode dans MyVisitor
laquelle accepte un paramètre de typeTrainNode
... ". Le compilateur, au moment de la compilation, détermine quelle est la méthode qui sera appelée. (Si Java inspectait effectivement les types dynamiques des arguments, les performances seraient assez terribles.)
Java nous donne un outil pour prendre en compte le type d'exécution (c'est-à-dire dynamique) d'un objet lorsqu'une méthode est appelée - répartition de méthode virtuelle . Lorsque nous appelons une méthode virtuelle, l'appel va en fait à une table en mémoire qui se compose de pointeurs de fonction. Chaque type a une table. Si une méthode particulière est remplacée par une classe, l'entrée de la table des fonctions de cette classe contiendra l'adresse de la fonction remplacée. Si la classe ne remplace pas une méthode, elle contiendra un pointeur vers l'implémentation de la classe de base. Cela entraîne toujours une surcharge de performances (chaque appel de méthode déréférencera essentiellement deux pointeurs: un pointant vers la table de fonctions du type et un autre sur la fonction elle-même), mais c'est toujours plus rapide que d'avoir à inspecter les types de paramètres.
L'objectif du modèle de visiteur est d'accomplir une double répartition - non seulement le type de cible d'appel est-il pris en compte ( MyVisitor
, via des méthodes virtuelles), mais également le type de paramètre (quel type de cible Node
regardons-nous)? Le modèle Visiteur nous permet de le faire par la combinaison visit
/ accept
.
En changeant notre ligne en ceci:
root.accept(new MyVisitor());
Nous pouvons obtenir ce que nous voulons: via la répartition de la méthode virtuelle, nous entrons le bon appel accept () tel qu'implémenté par la sous-classe - dans notre exemple avec TrainElement
, nous entrerons TrainElement
l'implémentation de accept()
:
class TrainNode extends Node implements IVisitable {
void accept(IVisitor v) {
v.visit(this);
}
}
Que sait le compilateur à ce stade, dans le cadre de TrainNode
's accept
? Il sait que le type statique de this
est aTrainNode
. Il s'agit d'une information supplémentaire importante dont le compilateur n'était pas au courant dans la portée de notre appelant: là, tout ce qu'il savait, root
c'était qu'il s'agissait d'un fichier Node
. Maintenant, le compilateur sait que this
( root
) n'est pas seulement un Node
, mais c'est en fait un TrainNode
. En conséquence, une ligne trouvé à l' intérieur accept()
: v.visit(this)
, signifie quelque chose d' autre. Le compilateur recherchera maintenant une surcharge de visit()
qui prend un TrainNode
. S'il n'en trouve pas, il compilera alors l'appel à une surcharge qui prend unNode
. Si aucun n'existe, vous obtiendrez une erreur de compilation (sauf si vous avez une surcharge qui prend object
). L'exécution entrera donc dans ce que nous avions toujours voulu: MyVisitor
la mise en œuvre de visit(TrainNode e)
. Aucun moulage n'était nécessaire et, surtout, aucune réflexion n'était nécessaire. Ainsi, la surcharge de ce mécanisme est plutôt faible: il ne se compose que de références de pointeur et rien d'autre.
Vous avez raison dans votre question - nous pouvons utiliser un casting et obtenir le bon comportement. Cependant, souvent, nous ne savons même pas quel est le type de Node. Prenons le cas de la hiérarchie suivante:
abstract class Node { ... }
abstract class BinaryNode extends Node { Node left, right; }
abstract class AdditionNode extends BinaryNode { }
abstract class MultiplicationNode extends BinaryNode { }
abstract class LiteralNode { int value; }
Et nous écrivions un simple compilateur qui analyse un fichier source et produit une hiérarchie d'objets conforme à la spécification ci-dessus. Si nous écrivions un interprète pour la hiérarchie implémentée en tant que visiteur:
class Interpreter implements IVisitor<int> {
int visit(AdditionNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left + right;
}
int visit(MultiplicationNode n) {
int left = n.left.accept(this);
int right = n.right.accept(this);
return left * right;
}
int visit(LiteralNode n) {
return n.value;
}
}
Le casting ne nous mènerait pas très loin, car nous ne connaissons ni les types left
ni right
les visit()
méthodes. Notre parseur renverrait probablement également un objet de type Node
qui pointait également vers la racine de la hiérarchie, nous ne pouvons donc pas non plus convertir cela en toute sécurité. Ainsi, notre interpréteur simple peut ressembler à:
Node program = parse(args[0]);
int result = program.accept(new Interpreter());
System.out.println("Output: " + result);
Le modèle de visiteur nous permet de faire quelque chose de très puissant: étant donné une hiérarchie d'objets, il nous permet de créer des opérations modulaires qui opèrent sur la hiérarchie sans avoir besoin de mettre le code dans la classe de la hiérarchie elle-même. Le modèle de visiteur est largement utilisé, par exemple, dans la construction du compilateur. Compte tenu de l'arborescence syntaxique d'un programme particulier, de nombreux visiteurs sont écrits qui opèrent sur cet arbre: vérification de type, optimisations, émission de code machine sont généralement implémentées en tant que visiteurs différents. Dans le cas du visiteur d'optimisation, il peut même générer un nouvel arbre de syntaxe étant donné l'arbre d'entrée.
Il a ses inconvénients, bien sûr: si nous ajoutons un nouveau type dans la hiérarchie, nous devons également ajouter une visit()
méthode pour ce nouveau type dans l' IVisitor
interface, et créer des implémentations stub (ou complètes) dans tous nos visiteurs. Nous devons également ajouter la accept()
méthode, pour les raisons décrites ci-dessus. Si la performance ne signifie pas grand-chose pour vous, il existe des solutions pour écrire des visiteurs sans avoir besoin du accept()
, mais elles impliquent normalement une réflexion et peuvent donc entraîner une surcharge assez importante.