Salut, je viens de réduire le prix de tous les produits. Préparons nos compétences en programmation pour l'ère post-COVID. Plus de détails »

Visiteur

Alias : Visitor

Intention

Visiteur est un patron de conception comportemental qui vous permet de séparer les algorithmes et les objets sur lesquels ils opèrent.

Patron de conception visiteur

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.

Export du graphe en XML

Export du graphe en XML.

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 mais é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.

La méthode d’export en XML devait être ajoutée dans toutes les classes nœud

La méthode d’export XML devait être ajoutée dans toutes les classes nœud, ce qui risquait de compromettre l’intégrité du code de l’application.

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 :

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

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 !

foreach (Node node in graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node)
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node)
    // ...
}

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.

// Code client
foreach (Node node in graph)
    node.accept(exportVisitor)

// City
class City is
    method accept(Visitor v) is
        v.doForCity(this)
    // ...

// Industry
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this)
    // ...

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

Agent d’assurance

Un bon agent d’assurance est toujours prêt à proposer différents contrats pour différentes organisations.

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

Structure du patron de conception visiteurStructure du patron de conception visiteur
  1. 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.

  2. Chaque Visiteur Concret implémente plusieurs versions des mêmes comportements, en fonction des classes des éléments concrets.

  3. 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.

  4. 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.

  5. 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.

Structure de l’exemple utilisé pour le patron de conception visiteur

Exporter différents types d’objets vers le format XML à l’aide d’un objet visiteur.

// L’interface élément déclare une méthode `accepter` qui prend
// l’interface de base visiteur en argument.
interface Shape is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

// Chaque classe concrète Élément doit implémenter la méthode
// `accepter` et la faire appeler la méthode du visiteur qui
// correspond à la classe de l’élément.
class Dot implements Shape is
    // ...

    // Vous remarquerez que nous appelons `visiterPoint`, ce qui
    // correspond au nom de la classe actuelle. Ainsi, nous
    // fournissons le nom de la classe de l’élément au visiteur
    // qui le manipule.
    method accept(v: Visitor) is
        v.visitDot(this)

class Circle implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this)

class Rectangle implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitRectangle(this)

class CompoundShape implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitCompoundShape(this)


// L’interface visiteur déclare un ensemble de méthodes visiter
// qui correspondent aux classes élément. La signature de la
// méthode visiter permet au visiteur d’identifier la classe
// exacte de l’élément qu’il manipule.
interface Visitor is
    method visitDot(d: Dot)
    method visitCircle(c: Circle)
    method visitRectangle(r: Rectangle)
    method visitCompoundShape(cs: CompoundShape)

// Les visiteurs concrets implémentent plusieurs versions du
// même algorithme. Ce dernier fonctionne avec toutes les
// classes concrètes Élément.
//
// Vous tirez tous les bénéfices du patron de conception
// visiteur lorsque vous l’utilisez avec une structure complexe
// d’objets, comme une arborescence. Dans ce cas, il peut être
// pratique de stocker certains états intermédiaires de
// l’algorithme tout en exécutant les méthodes du visiteur sur
// les différents objets de la structure.
class XMLExportVisitor implements Visitor is
    method visitDot(d: Dot) is
        // Exporte l’ID du point et ses coordonnées.

    method visitCircle(c: Circle) is
        // Exporte l’ID du cercle, les coordonnées de son centre
        // et son rayon.

    method visitRectangle(r: Rectangle) is
        // Exporte l’ID du rectangle, les coordonnées du point
        // supérieur gauche, ainsi que sa largeur et sa
        // longueur.

    method visitCompoundShape(cs: CompoundShape) is
        // Exporte l’ID de la forme, ainsi qu’une liste des ID
        // de ses enfants.


// Le code client peut lancer des traitements du visiteur sur
// n’importe quel ensemble d’éléments sans connaître leurs
// classes concrètes. La méthode accepter envoie un appel au
// traitement approprié de l’objet visiteur.
class Application is
    field allShapes: array of Shapes

    method export() is
        exportVisitor = new XMLExportVisitor()

        foreach (shape in allShapes) do
            shape.accept(exportVisitor)

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

Exemples de code

Visiteur en C# Visiteur en C++ Visiteur en Go Visiteur en Java Visiteur en PHP Visiteur en Python Visiteur en Ruby Visiteur en Rust Visiteur en Swift Visiteur en TypeScript

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.