Visiteur
Intention
Visiteur est un patron de conception comportemental qui vous permet de séparer les algorithmes et les objets sur lesquels ils opèrent.
Problème
Imaginez que votre équipe développe une application avec des informations géographiques qui prennent la forme d’un graphe géant. Chaque nœud du graphe peut représenter une entité complexe comme une ville, mais aussi des choses plus spécifiques comme des usines, des sites touristiques, etc. Les nœuds sont interconnectés s’il est possible de les relier. Dans le code, chaque type de nœud est représenté par sa propre classe et chaque nœud spécifique est un objet.
Le jour vient où vous devez vous attaquer à la mise en place de l’export du graphe au format XML. À première vue, cela semble assez simple. Vous avez prévu de mettre en place une méthode d’export pour chaque classe nœud, puis vous exécutez la méthode sur chaque nœud du graphe en le parcourant récursivement. La solution était simple et élégante : grâce au polymorphisme, vous n’avez pas couplé le code qui appelait la méthode d’exportation aux classes concrètes des nœuds.
Malheureusement, l’architecte du système vous a interdit de modifier les classes nœud existantes. Sa décision était basée sur le fait que le code était déjà en production et que vos modifications pourraient causer des bugs.
De plus, il a remis en question la pertinence de placer le code d’export XML à l’intérieur des nœuds. Le rôle principal de ces classes est de manipuler les données géographiques, ce code serait perçu comme un intrus.
Il a avancé un autre argument contre votre modification. Il semblait très probable qu’une fois cette fonctionnalité en place, une personne du service marketing demande un export dans un format différent ou d’autres trucs bizarres. Cela vous obligerait à modifier une fois de plus ces petites classes fragiles.
Solution
Le patron de conception visiteur vous propose de placer ce nouveau comportement dans une classe séparée que l’on appelle visiteur, plutôt que de l’intégrer dans des classes existantes. L’objet qui devait lancer ce traitement à l’origine est maintenant passé en paramètre des méthodes du visiteur, ce qui permet à la méthode d’avoir accès à toutes les données nécessaires qui se trouvent à l’intérieur de l’objet.
Comment fait-on pour que ce comportement puisse être exécuté sur des objets de différentes classes ? Par exemple, dans le cas de notre export XML, l’implémentation sera probablement légèrement différente pour chaque nœud. La classe visiteur va donc avoir besoin d’un ensemble de méthodes et chacune d’entre elles pourra prendre des paramètres de différents types, comme ce qui suit :
Mais comment allons-nous appeler ces méthodes, surtout celles qui gèrent le graphe complet ? Nous ne pouvons pas utiliser le polymorphisme, car ces méthodes ont différentes signatures. Pour sélectionner une méthode qui peut traiter un objet donné, nous devons vérifier sa classe. On se croirait dans un cauchemar !
Il vous est peut-être venu à l’esprit d’utiliser la surcharge. La surcharge, c’est une technique qui permet de donner le même nom à toutes les méthodes, même si elles n’ont pas des paramètres identiques. Malheureusement, même si l’on suppose que notre langage de programmation nous le permet (le Java et le C# par exemple), cela ne nous sera d’aucune aide. Comme nous ne connaissons pas la classe d’un nœud à l’avance, le mécanisme de la surcharge ne sera pas capable de déterminer la bonne méthode à exécuter. Il ira systématiquement chercher la méthode qui prend un objet de la classe de base Nœud
.
Heureusement, le patron de conception visiteur résout ce problème. Il utilise une technique appelée double répartition (double dispatch), qui aide à lancer la bonne méthode sans s’encombrer avec des blocs conditionnels. Plutôt que de laisser le client choisir la version de la méthode à appeler, pourquoi ne déléguons-nous pas la décision aux objets que nous passons en paramètre au visiteur ? Comme les objets connaissent leur propre classe, ils seront plus à même de choisir la méthode adaptée au visiteur. Ils « acceptent » un visiteur et lui indiquent la méthode à exécuter.
J’avoue. Nous avons finalement été obligés de toucher aux classes nœud. Mais cette modification est mineure et elle nous permet d’ajouter de nouveaux comportements sans avoir à retoucher le code.
À présent, si nous extrayons une interface pour tous les visiteurs, les nœuds existants vont accepter tous les visiteurs ajoutés dans l’application. Pour ajouter un nouveau comportement aux nœuds, il vous suffit d’implémenter une nouvelle classe visiteur.
Analogie
Imaginez un agent d’assurance expérimenté qui veut absolument acquérir de nouveaux clients. Il peut faire du porte-à-porte dans tous les bâtiments d’un quartier et essayer de vendre des assurances à tous ceux qu’il rencontre. En fonction du type d’entreprise qui occupe le bâtiment, il peut proposer des contrats d’assurance adaptés :
- Si c’est un bâtiment résidentiel, il vend des assurances maladie.
- Si c’est une banque, il vend des assurances contre le vol.
- Si c’est un café, il vend des assurances contre les incendies et les inondations.
Structure
-
L’interface Visiteur déclare un ensemble de méthodes de parcours qui peuvent prendre les éléments concrets d’une structure d’objets en paramètre. Ces méthodes peuvent avoir le même nom si le programme est écrit dans un langage qui gère la surcharge, mais le type de ses paramètres sera différent.
-
Chaque Visiteur Concret implémente plusieurs versions des mêmes comportements, en fonction des classes des éléments concrets.
-
L’interface Élément déclare une méthode qui « accepte » les visiteurs. Cette méthode déclare un paramètre du type de l’interface visiteur.
-
Chaque Élément Concret doit implémenter une méthode d’acceptation. Le but de cette méthode est de rediriger l’appel vers la méthode appropriée du visiteur en fonction de la classe de l’élément actuel. Soyez conscient que même si la classe d’un élément de base implémente cette méthode, toutes les sous-classes doivent tout de même la redéfinir et appeler la méthode appropriée sur l’objet visiteur.
-
Le Client représente en général une collection ou tout autre objet complexe (par exemple un arbre Composite). En général, les clients n’ont pas de visibilité sur les classes des éléments concrets, car ils manipulent les objets de cette collection via une interface abstraite.
Pseudo-code
Dans cet exemple, le Visiteur implémente l’export XML dans la hiérarchie de classes des formes géométriques.
Si vous voulez savoir pourquoi nous avons besoin d’une méthode accepter
dans cet exemple, vous pouvez consulter mon article Visiteur et Double répartition qui décrit le sujet en détail.
Possibilités d’application
Utilisez le visiteur lorsque vous voulez lancer des traitements sur les éléments d’un objet ayant une structure complexe (une arborescence par exemple).
Le patron de conception visiteur vous permet de lancer des traitements sur un ensemble d’objets de différentes classes à l’aide d’un objet visiteur qui implémente une variante d’un même traitement pour chaque classe visée.
Utilisez le visiteur pour nettoyer la logique métier de tous les comportements secondaires.
Ce patron vous permet de spécialiser encore plus les classes principales de votre application, en transférant les autres comportements dans des classes visiteur.
Utilisez le visiteur si un comportement n’est adapté que pour certaines classes d’une hiérarchie de classes, mais pas pour les autres.
Vous pouvez envoyer ce comportement dans une classe visiteur séparée, implémenter seulement les méthodes de visite qui acceptent les objets des classes concernées et laisser le reste vide.
Mise en œuvre
-
Déclarez l’interface visiteur avec un ensemble de méthodes « visiter » ; une pour chaque classe d’élément concret qui existe dans le programme.
-
Déclarez l’interface élément. Si vous avez déjà une hiérarchie de classes élément, ajoutez la méthode abstraite « accepter » à la classe de base de la hiérarchie. Cette méthode doit prendre un Visiteur en paramètre.
-
Implémentez les méthodes d’acceptation dans toutes les classes des éléments concrets. Ces méthodes doivent simplement rediriger l’appel vers la méthode de visite de l’objet visiteur qui correspond à la classe de l’élément actuel.
-
Les classes élément doivent uniquement interagir avec les visiteurs via l’interface visiteur. En revanche, les visiteurs doivent avoir la visibilité sur toutes les classes des éléments concrets, qui sont référencés comme les types des paramètres des méthodes de visite.
-
Pour chaque comportement qui ne peut être écrit à l’intérieur de la hiérarchie des éléments, créez une nouvelle classe concrète Visiteur et implémentez toutes les méthodes de visite.
Vous pourriez vous retrouver dans une situation où le visiteur aura besoin d’un accès aux membres privés d’une classe élément. Dans ce cas, vous pouvez rendre ces attributs ou méthodes publics (ce qui ne respecte pas l’encapsulation de l’élément) ou imbriquer la classe visiteur dans la classe de l’élément. Cette dernière possibilité n’est envisageable que si votre langage de programmation gère les classes imbriquées.
-
Le client doit créer des objets visiteur et les passer dans des éléments via des méthodes « accepter ».
Avantages et inconvénients
- Principe ouvert/fermé. Vous pouvez ajouter un nouveau comportement qui acceptera les objets de différentes classes sans les modifier.
- Principe de responsabilité unique. Vous pouvez déplacer plusieurs versions du même comportement dans une seule classe.
- Un objet visiteur peut accumuler des informations utiles en manipulant différents objets. Cela peut se révéler pratique si vous voulez parcourir une structure complexe d’objets comme un arbre, et lancer le traitement du visiteur sur chaque objet de cette structure.
- Vous devez mettre à jour les visiteurs chaque fois qu’une classe est ajoutée ou retirée de la hiérarchie des éléments.
- Les visiteurs n’ont parfois pas les accès nécessaires aux attributs ou méthodes privés des éléments qu’ils sont censés manipuler.
Liens avec les autres patrons
-
Vous pouvez traiter le Visiteur comme une version plus puissante du patron de conception Commande. Ses objets peuvent lancer des traitements sur divers objets dans différentes classes.
-
Vous pouvez utiliser le Visiteur pour lancer une opération sur un arbre Composite entier.
-
Vous pouvez utiliser le Visiteur avec l’Itérateur pour parcourir une structure de données complexe et lancer un traitement sur ses éléments, même s’ils ont des classes différentes.
Informations supplémentaires
- Vous demandez-vous toujours pourquoi vous ne pouvez pas remplacer le patron de conception visiteur par la surcharge ? Lisez mon article Visiteur et double répartition pour mieux comprendre ces vilains détails.