Le modèle Visiteur est un modèle de conception logicielle qui sépare l' algorithme de la structure objet . Grâce à cette séparation, de nouvelles opérations peuvent être ajoutées aux structures objet existantes sans les modifier. C'est une façon d'appliquer le principe ouvert/fermé en programmation orientée objet et en génie logiciel .
En résumé, le visiteur permet d'ajouter de nouvelles fonctions virtuelles à une famille de classes sans modifier ces dernières. Une classe visiteur est créée, implémentant toutes les spécialisations appropriées de la fonction virtuelle. Le visiteur prend en entrée la référence de l'instance et implémente l'objectif par double dispatch .
Les langages de programmation avec types somme et correspondance de modèles rendent caduques de nombreux avantages du modèle visiteur, car la classe visiteur peut à la fois facilement se brancher sur le type de l'objet et générer une erreur de compilation si un nouveau type d'objet est défini et que le visiteur ne le gère pas encore.
Aperçu
Le modèle de conception Visitor est l'un des vingt-trois modèles de conception du Gang des Quatre .
Problèmes que le modèle peut résoudre
- Il devrait être possible de définir une nouvelle opération pour (certaines) classes d'une structure d'objet sans modifier les classes.
Lorsque de nouvelles opérations sont fréquemment nécessaires et que la structure d'objet est composée de nombreuses classes non liées, il est inflexible d'ajouter de nouvelles sous-classes chaque fois qu'une nouvelle opération est requise car « la distribution de toutes ces opérations à travers les différentes classes de nœuds conduit à un système difficile à comprendre, à maintenir et à modifier ».
Solution décrite par le modèle
- Définissez un objet (visiteur) distinct qui implémente une opération à effectuer sur les éléments d'une structure d'objet.
- Les clients parcourent la structure des objets et appellent une opération de répartition accept (visitor) sur un élément — qui « répartit » (délègue) la requête à « l'objet visiteur accepté ». L'objet visiteur effectue ensuite l'opération sur l'élément (« visite l'élément »).
Cela permet de créer de nouvelles opérations indépendamment des classes d'une structure d'objets en ajoutant de nouveaux objets visiteurs.
Voir également le diagramme de classes et de séquences UML ci-dessous.
Définition
Le Gang des Quatre définit le Visiteur comme :
Représente une opération à effectuer sur les éléments d'une structure d'objets. Le visiteur permet de définir une nouvelle opération sans modifier les classes des éléments sur lesquels elle agit.
La nature du Visiteur en fait un modèle idéal pour s'intégrer aux API publiques, permettant ainsi à ses clients d'effectuer des opérations sur une classe en utilisant une classe «visitante» sans avoir à modifier le code source.
Avantages
Le transfert des opérations vers des classes de visiteurs est bénéfique lorsque
- de nombreuses opérations sans lien entre elles sur une structure d'objet sont nécessaires,
- Les classes qui composent la structure objet sont connues et ne devraient pas changer.
- De nouvelles opérations doivent être ajoutées fréquemment.
- Un algorithme implique plusieurs classes de la structure d'objets, mais on souhaite le gérer à un seul endroit.
- Un algorithme doit fonctionner sur plusieurs hiérarchies de classes indépendantes.
L'inconvénient de ce modèle est qu'il rend les extensions de la hiérarchie des classes plus difficiles, car les nouvelles classes nécessitent généralement visitl'ajout d'une nouvelle méthode à chaque visiteur.
Application
Prenons l'exemple d'un système de conception assistée par ordinateur (CAO) 2D. Ce système repose sur plusieurs types d'entités représentant des formes géométriques de base telles que des cercles, des lignes et des arcs. Ces entités sont organisées en calques, et au sommet de la hiérarchie des types se trouve le dessin, qui n'est autre qu'une liste de calques, enrichie de propriétés supplémentaires.
Une opération fondamentale de cette hiérarchie de types consiste à enregistrer un dessin au format de fichier natif du système. De prime abord, il peut sembler judicieux d'ajouter des méthodes d'enregistrement locales à tous les types de la hiérarchie. Cependant, il peut également être utile d'enregistrer les dessins dans d'autres formats de fichier. Multiplier les méthodes d'enregistrement pour différents formats de fichier risque de complexifier la structure de données géométriques d'origine.
Une solution simpliste consisterait à maintenir des fonctions distinctes pour chaque format de fichier. Une telle fonction d'enregistrement prendrait un dessin en entrée, le parcourrait et l'encoderait dans le format de fichier spécifique. Cependant, cette opération étant répétée pour chaque nouveau format, la duplication entre les fonctions s'accumule. Par exemple, l'enregistrement d'un cercle au format raster nécessite un code très similaire quel que soit le format raster utilisé, contrairement à l'enregistrement d'autres formes primitives. Le cas est similaire pour les lignes et les polygones. Le code se transforme alors en une grande boucle externe parcourant les objets, avec un arbre de décision interne important déterminant le type de l'objet. Un autre problème de cette approche est le risque d'oublier une forme dans une ou plusieurs fonctions d'enregistrement, ou d'introduire une nouvelle forme primitive sans implémenter la routine d'enregistrement pour un seul type de fichier, ce qui engendre des problèmes d'extension et de maintenance du code. Plus les versions d'un même fichier se multiplient, plus sa maintenance devient complexe.
On peut alors appliquer le modèle Visiteur. Il encode l'opération logique (c.-à-d. save(image_tree)) sur l'ensemble de la hiérarchie dans une seule classe (c.-à-d. Saver) qui implémente les méthodes communes de parcours de l'arbre et décrit les méthodes auxiliaires virtuelles (c.-à-d. save_circle, save_square, etc.) à implémenter pour les comportements spécifiques au format. Dans l'exemple CAO, ces comportements spécifiques seraient implémentés par une sous-classe de Visiteur (c.-à-d. SaverPNG). Ainsi, toute duplication des vérifications de type et des étapes de parcours est supprimée. De plus, le compilateur signale désormais une erreur si une forme est omise, car elle est maintenant attendue par la fonction de parcours/sauvegarde de base.
Boucles d'itération
Le modèle Visiteur peut être utilisé pour itérer sur des structures de données de type conteneur , tout comme le modèle Itérateur, mais avec des fonctionnalités limitées. Par exemple, l'itération sur une structure de répertoires pourrait être implémentée par une classe de fonction plutôt que par le modèle de boucle plus conventionnel . Cela permettrait d'extraire diverses informations utiles du contenu des répertoires en implémentant une fonctionnalité Visiteur pour chaque élément, tout en réutilisant le code d'itération. Ce modèle est largement utilisé dans les systèmes Smalltalk et se rencontre également en C++. Un inconvénient de cette approche est qu'il est difficile de sortir de la boucle ou d'itérer de manière concurrente (en parallèle, c'est-à-dire en parcourant deux conteneurs simultanément avec une seule ivariable). Cette dernière option nécessiterait l'écriture de fonctionnalités supplémentaires pour le Visiteur.
Structure
Diagramme de classes et de séquence UML

Dans le diagramme de classes UML ci-dessus, la classe n'implémente pas directement une nouvelle opération. Elle implémente plutôt une opération de répartition qui « répartit » (délègue) une requête à l'« objet visiteur accepté » ( ). La classe implémente l'opération ( ). puis implémente en répartissant vers . La classe implémente l'opération ( ). ElementAElementAaccept(visitor)visitor.visitElementA(this)Visitor1visitElementA(e:ElementA)ElementBaccept(visitor)visitor.visitElementB(this)Visitor1visitElementB(e:ElementB)
Le diagramme de séquence UML illustre les interactions d'exécution : l' objet parcourt les éléments d'une structure d'objets et appelle chaque élément. Tout d'abord, il appelle , qui appelle l' objet accepté. L'élément lui-même est transmis à afin qu'il puisse le « visiter » (appel à ). Ensuite, il appelle , qui appelle l'objet qui le « visite » (appels à ). ClientElementA,ElementBaccept(visitor)Clientaccept(visitor)ElementAvisitElementA(this)visitorthisvisitorElementAoperationA()Clientaccept(visitor)ElementBvisitElementB(this)visitorElementBoperationB()
Diagramme de classes


Détails
Le modèle Visiteur requiert un langage de programmation prenant en charge l'appel unique , comme c'est le cas pour les langages orientés objet courants (tels que C++ , Java , Smalltalk , Objective-C , Swift , JavaScript , Python et C# ). Dans ce cas, considérons deux objets, chacun d'un certain type de classe ; l'un est appelé l' élément , et l'autre le visiteur .
Objets
Visiteur
Le visiteur déclare une visitméthode prenant l'élément comme argument, pour chaque classe d'élément. Les visiteurs concrets héritent de la classe visiteur et implémentent ces visitméthodes, chacune implémentant une partie de l'algorithme agissant sur la structure de l'objet. L'état de l'algorithme est géré localement par la classe visiteur concrète.
Élément
L' élément déclare une acceptméthode permettant d'accepter un visiteur, en prenant ce dernier comme argument. Les éléments concrets , dérivés de la classe `element`, implémentent cette acceptméthode. Dans sa forme la plus simple, il s'agit simplement d'un appel à la méthode du visiteur visit. Les éléments composites , qui gèrent une liste d'objets enfants, itèrent généralement sur cette liste, en appelant la méthode de chaque enfant accept.
Client
Le client crée la structure objet, directement ou indirectement, et instancie les visiteurs concrets. Lorsqu'une opération implémentée selon le modèle Visiteur doit être effectuée, il appelle la acceptméthode du ou des éléments de niveau supérieur.
Méthodes
Accepter
Lors de acceptl'appel de la méthode dans le programme, son implémentation est choisie en fonction du type dynamique de l'élément et du type statique du visiteur. Lors de visitl'appel de la méthode associée, son implémentation est choisie en fonction du type dynamique du visiteur et du type statique de l'élément, tel que connu depuis l'implémentation de la acceptméthode, qui est identique au type dynamique de l'élément. (De plus, si le visiteur ne peut pas traiter un argument du type de l'élément donné, le compilateur détectera l'erreur.)
Visite
Ainsi, l'implémentation de la visitméthode est choisie en fonction du type dynamique de l'élément et du type dynamique du visiteur. Ceci implémente effectivement une double dispatch . Pour les langages dont le système objet prend en charge la dispatch multiple, et non la dispatch unique, comme Common Lisp ou C# via le Dynamic Language Runtime (DLR), l'implémentation du modèle Visiteur est grandement simplifiée (également appelée Visiteur dynamique) grâce à l'utilisation d'une surcharge de fonction simple permettant de couvrir tous les cas visités. Un visiteur dynamique, à condition qu'il opère uniquement sur des données publiques, respecte le principe ouvert/fermé (puisqu'il ne modifie pas les structures existantes) et le principe de responsabilité unique (puisqu'il implémente le modèle Visiteur dans un composant distinct).
De cette manière, un algorithme peut être écrit pour parcourir un graphe d'éléments, et de nombreux types d'opérations différents peuvent être effectués au cours de ce parcours en fournissant différents types de visiteurs pour interagir avec les éléments en fonction des types dynamiques des éléments et des visiteurs.
Exemples
C#
Cet exemple déclare une ExpressionPrintingVisitorclasse distincte qui gère l'impression. Si l'on souhaite introduire un nouveau visiteur concret, une nouvelle classe sera créée pour implémenter l'interface Visitor, et de nouvelles implémentations des méthodes Visit seront fournies. Les classes existantes (Literal et Addition) resteront inchangées.
espace de noms Wikipedia.Exemples ;utiliser le système ;interface IVisitor { void Visit ( Literal literal ); void Visit ( Addition addition ); }class ExpressionPrintingVisitor : IVisitor { public void Visit ( Literal literal ) { Console . WriteLine ( literal . Value ); }public void Visit ( Addition addition ) { double leftValue = addition.Left.GetValue ( ); double rightValue = addition.Right.GetValue ( ) ; double sum = addition.GetValue ( ) ; Console.WriteLine ( $ "{ leftValue } + { rightValue} = { sum } " ) ; } }classe abstraite Expression { public abstrait void Accept ( IVisitor visiteur ); public abstrait double GetValue (); }class Literal : Expression { public Literal ( double valeur ) { this . Valeur = valeur ; }public double Valeur { obtenir ; définir ; }public override void Accept ( IVisitor visitor ) { visitor . Visit ( this ); } public override double GetValue () { return Value ; } }classe Addition : Expression { public Addition ( Expression gauche , Expression droite ) { Gauche = gauche ; Droite = droite ; }Expression publique Gauche { obtenir ; définir ; } Expression publique Droite { obtenir ; définir ; }public override void Accept ( IVisitor visitor ) { Left.Accept ( visitor ) ; Right.Accept ( visitor ) ; visitor.Visit ( this ) ; } public override double GetValue ( ) { return Left.GetValue ( ) + Right.GetValue ( ) ; } }public static class Program { public static void Main ( string [] args ) { // Émuler l' addition 1 + 2 + 3 e = new ( new Addition ( new Literal ( 1 ), new Literal ( 2 ) ), new Literal ( 3 ) );ExpressionPrintingVisitor printingVisitor = new (); e . Accept ( printingVisitor ); Console . ReadKey (); } }
Conversation
Dans ce cas, il incombe à l'objet de savoir comment s'afficher sur un flux. Le visiteur est donc l'objet, et non le flux.
"Il n'existe pas de syntaxe pour créer une classe. Les classes sont créées en envoyant des messages à d'autres classes." Sous-classe WriteStream : #ExpressionPrinter instanceVariableNames : '' classVariableNames : '' package : 'Wikipedia' .ExpressionPrinter >>write: anObject "Délègue l'action à l'objet. L'objet n'a pas besoin d'être d'une classe spéciale ; il doit seulement être capable de comprendre le message #putOn:" anObject putOn: self . ^ anObject .sous-classe d'objet : #Expression instanceVariableNames : '' classVariableNames : '' package : 'Wikipedia' .Sous-classe d'expression : #Literal instanceVariableNames : 'value' classVariableNames : '' package : 'Wikipedia' .Classe Literal >> avec : aValue "Méthode de classe pour créer une instance de la classe Literal" ^ self nouvelle valeur : aValue ; vous-même .Littéral >>valeur : aValue "Setter pour valeur" valeur := aValue .Literal >>putOn: aStream "Un objet Literal sait comment s'imprimer lui-même" aStream nextPutAll: value asString .Sous-classe d'expression : #Addition instanceVariableNames : 'gauche droite' classVariableNames : '' package : 'Wikipedia' .Classe Addition >> gauche : a droite : b "Méthode de classe pour créer une instance de la classe Addition" ^ self new gauche : a ; droite : b ; vous-même .Addition >>gauche : une expression "Setter pour gauche" gauche := une expression .Addition >>droite : uneExpression "Setter pour droite" droite := uneExpression .Addition >>putOn: aStream "Un objet Addition sait comment s'imprimer lui-même" aStream nextPut: $( . left putOn: aStream . aStream nextPut: $+ . right putOn: aStream . aStream nextPut: $) .sous-classe d'objet : #Program instanceVariableNames : '' classVariableNames : '' package : 'Wikipedia' .Programme >> principal | flux d'expression | expression := Addition gauche : ( Addition gauche : ( Littéral avec : 1 ) droite : ( Littéral avec : 2 )) droite : ( Littéral avec : 3 ) . flux := ExpressionPrinter activé : ( Chaîne nouvelle : 100 ) . flux écrire : expression . Transcription afficher : contenu du flux . Transcription vider .
Aller
Go ne prend pas en charge la surcharge de méthodes ; les méthodes de visite doivent donc avoir des noms différents. Une interface de visiteur typique pourrait être :
type Visitor interface { visitWheel ( wheel Wheel ) string visitEngine ( engine Engine ) string visitBody ( body Body ) string visitCar ( car Car ) string }
Java
L'exemple suivant, écrit en Java , illustre l'affichage du contenu d'une arborescence de nœuds (décrivant ici les composants d'une voiture). Au lieu de créer printdes méthodes pour chaque sous-classe de nœud ( par exemple Wheel, `Car` Engine, Body`Car` et `Car` Car), une seule classe visiteur CarElementPrintVisitoreffectue l'affichage. Comme les différentes sous-classes de nœuds nécessitent des actions légèrement différentes pour un affichage correct, CarElementPrintVisitorla classe visiteur répartit les actions en fonction de la classe de l'argument passé à sa visitméthode. La méthode `print CarElementDoVisitor()`, analogue à une opération d'enregistrement pour un autre format de fichier, fonctionne de la même manière.
Diagramme
