On parle beaucoup de découpler les algorithmes des classes. Mais, une chose reste de côté non expliquée.
Ils utilisent le visiteur comme ça
abstract class Expr {
public <T> T accept(Visitor<T> visitor) {visitor.visit(this);}
}
class ExprVisitor extends Visitor{
public Integer visit(Num num) {
return num.value;
}
public Integer visit(Sum sum) {
return sum.getLeft().accept(this) + sum.getRight().accept(this);
}
public Integer visit(Prod prod) {
return prod.getLeft().accept(this) * prod.getRight().accept(this);
}
Au lieu d'appeler directement visit (element), Visitor demande à l'élément d'appeler sa méthode visit. Cela contredit l'idée déclarée d'inconscience de classe sur les visiteurs.
PS1 Veuillez expliquer avec vos propres mots ou indiquer une explication exacte. Parce que deux réponses que j'ai reçues font référence à quelque chose de général et d'incertain.
PS2 Ma conjecture: puisque getLeft()
retourne la base Expression
, l'appel visit(getLeft())
entraînerait visit(Expression)
, tandis que l' getLeft()
appel visit(this)
entraînerait un autre appel de visite, plus approprié. Donc, accept()
effectue la conversion de type (aka casting).
La correspondance de modèle de PS3 Scala = modèle de visiteur sur stéroïde montre à quel point le modèle de visiteur est plus simple sans la méthode d'acceptation. Wikipedia ajoute à cette déclaration : en reliant un article montrant "que les accept()
méthodes ne sont pas nécessaires lorsque la réflexion est disponible; introduit le terme" Walkabout "pour la technique."
Réponses:
Les structures
visit
/ du modèle de visiteuraccept
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:C'est ici que se trouve le problème. Si notre
MyVisitor
classe était définie comme suit:Si, au moment de l'exécution, quel que soit le type réel
root
, notre appel irait dans la surchargevisit(Node node)
. Cela serait vrai pour toutes les variables déclarées de typeNode
. 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 deroot
? Oh, je vois. C'est unTrainNode
. Voyons s'il y a une méthode dansMyVisitor
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 cibleNode
regardons-nous)? Le modèle Visiteur nous permet de le faire par la combinaisonvisit
/accept
.En changeant notre ligne en ceci:
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 entreronsTrainElement
l'implémentation deaccept()
:Que sait le compilateur à ce stade, dans le cadre de
TrainNode
'saccept
? Il sait que le type statique dethis
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 fichierNode
. Maintenant, le compilateur sait quethis
(root
) n'est pas seulement unNode
, mais c'est en fait unTrainNode
. En conséquence, une ligne trouvé à l' intérieuraccept()
:v.visit(this)
, signifie quelque chose d' autre. Le compilateur recherchera maintenant une surcharge devisit()
qui prend unTrainNode
. 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 prendobject
). L'exécution entrera donc dans ce que nous avions toujours voulu:MyVisitor
la mise en œuvre devisit(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:
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:
Le casting ne nous mènerait pas très loin, car nous ne connaissons ni les types
left
niright
lesvisit()
méthodes. Notre parseur renverrait probablement également un objet de typeNode
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 à: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 laaccept()
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 duaccept()
, mais elles impliquent normalement une réflexion et peuvent donc entraîner une surcharge assez importante.la source
accept()
méthode devient nécessaire lorsque cet avertissement est violé dans le visiteur.Bien sûr, ce serait idiot si c'était la seule façon dont Accept est implémenté.
Mais ce n'est pas.
Par exemple, les visiteurs sont vraiment très utiles lorsqu'ils traitent des hiérarchies, auquel cas l'implémentation d'un nœud non terminal pourrait être quelque chose comme ça
Vous voyez? Ce que vous décrivez comme stupide est la solution pour traverser les hiérarchies.
Voici un article beaucoup plus long et approfondi qui m'a fait comprendre le visiteur .
Edit: Pour clarifier: La
Visit
méthode du visiteur contient une logique à appliquer à un nœud. LaAccept
méthode du nœud contient une logique sur la façon de naviguer vers les nœuds adjacents. Le cas où vous ne faites que doubler la répartition est un cas particulier où il n'y a tout simplement aucun nœud adjacent vers lequel naviguer.la source
L'objectif du modèle Visiteur est de s'assurer que les objets savent quand le visiteur en a fini avec eux et sont partis, afin que les classes puissent effectuer tout nettoyage nécessaire par la suite. Cela permet également aux classes d'exposer leurs internes «temporairement» en tant que paramètres 'ref', et de savoir que les internes ne seront plus exposés une fois le visiteur parti. Dans les cas où aucun nettoyage n'est nécessaire, le modèle de visiteur n'est pas très utile. Les classes qui ne font aucune de ces choses peuvent ne pas bénéficier du modèle de visiteur, mais le code qui est écrit pour utiliser le modèle de visiteur sera utilisable avec les classes futures qui peuvent nécessiter un nettoyage après l'accès.
Par exemple, supposons que l'on ait une structure de données contenant de nombreuses chaînes qui devraient être mises à jour de manière atomique, mais que la classe contenant la structure de données ne sait pas précisément quels types de mises à jour atomiques doivent être effectuées (par exemple, si un thread veut remplacer toutes les occurrences de " X ", alors qu'un autre thread souhaite remplacer toute séquence de chiffres par une séquence numériquement supérieure d'un, les opérations des deux threads devraient réussir; si chaque thread lit simplement une chaîne, effectue ses mises à jour et la réécrit, le deuxième thread réécrire sa chaîne écraserait la première). Une façon d'accomplir cela serait de demander à chaque thread d'acquérir un verrou, d'effectuer son opération et de libérer le verrou. Malheureusement, si les verrous sont exposés de cette manière,
Le modèle Visiteur propose (au moins) trois approches pour éviter ce problème:
Sans le modèle de visiteur, effectuer des mises à jour atomiques nécessiterait d'exposer les verrous et de risquer une défaillance si le logiciel appelant ne parvient pas à suivre un protocole de verrouillage / déverrouillage strict. Avec le modèle Visiteur, les mises à jour atomiques peuvent être effectuées de manière relativement sûre.
la source
Les classes qui nécessitent une modification doivent toutes implémenter la méthode 'accept'. Les clients appellent cette méthode d'acceptation pour effectuer une nouvelle action sur cette famille de classes, étendant ainsi leurs fonctionnalités. Les clients peuvent utiliser cette méthode d'acceptation unique pour effectuer un large éventail de nouvelles actions en transmettant une classe de visiteur différente pour chaque action spécifique. Une classe de visiteur contient plusieurs méthodes de visite remplacées définissant comment réaliser cette même action spécifique pour chaque classe de la famille. Ces méthodes de visite reçoivent une instance sur laquelle travailler.
Les visiteurs sont utiles si vous ajoutez, modifiez ou supprimez fréquemment des fonctionnalités à une famille stable de classes car chaque élément de fonctionnalité est défini séparément dans chaque classe de visiteur et les classes elles-mêmes n'ont pas besoin d'être modifiées. Si la famille de classes n'est pas stable, le modèle de visiteur peut être moins utile, car de nombreux visiteurs doivent changer chaque fois qu'une classe est ajoutée ou supprimée.
la source
Un bon exemple est dans la compilation de code source:
Les clients peuvent mettre en œuvre un
JavaBuilder
,RubyBuilder
,XMLValidator
, etc. , et la mise en œuvre de la collecte et de visiter tous les fichiers source dans un projet n'a pas besoin de changer.Ce serait un mauvais modèle si vous avez des classes distinctes pour chaque type de fichier source:
Cela dépend du contexte et des parties du système que vous souhaitez extensibles.
la source