Accueil / Tutoriels / Visitor

Visitor

Publié le 31 Mars 2025
Conception

Visitor (visiteur en français) est un design pattern comportemental qui permet de séparer un algorithme de la structure de données sur laquelle il opère. Il est très utile pour ajouter de nouvelles fonctionnalités à un objet sans en modifier la structure.

Principe

L’idée est de déplacer des comportements à l’extérieur des objets sur lesquels un visiteur agit. Au lieu d’avoir plusieurs méthodes dans chaque classe pour des opérations différentes, un objet visiteur est défini.

Ainsi, on ajoute de nouveaux traitements sans devoir modifier les classes existantes. L’ensemble des traitements est centralisé dans une seule classe (le visiteur).

En programmation orienté objet, la plupart des langages utilise le concept de double dispatch. La méthode appelée est déterminée en fonction du type de l’objet qui reçoit l’appel.

Le design pattern Visitor repose sur ce concept de double dispatch. Deux décisions sont prises pour décider quelle méthode exécuter : le type de l’objet qui reçoit l’appel et le type du visiteur passé en argument. Ce mécanisme permet d’éviter les structures conditionnelles du type if ou switch.

Exemples d’utilisation du pattern Visitor

Génération de contenus dynamiques (HTML, JSON, etc..)

En fonction du format d'affichage souhaité, les éléments d’une page doivent être rendus différemment. Le pattern Visitor permet de séparer la logique d’affichage du modèle de données. Au lieu d’avoir une méthode de rendu pour le HTML renderHTML() et une autre pour le JSON renderJSON(), on va utiliser des visiteurs dédiés.

Par exemple pour le HTML, un visiteur HTMLRenderer et pour le JSON un visiteur JSONRenderer. Ensuite, chaque élément de la page (texte, image, vidéo, etc…) accepte un visiteur et va déléguer le rendu au bon Renderer.

Système de validation des données

Dans une application web, il arrive de gérer des formulaires complexes où chaque champ à des règles de validation spécifiques (TextField, EmailField, etc…). Au lieu d’utiliser des conditions if ou switch, on peut créer un visiteur pour la validation qui va implémenter une logique de validation pour chaque type de champ. Si on doit ajouter par exemple un champ PhoneField, il suffira d’ajouter une méthode dans le visiteur sans devoir modifier la logique des autres champs.

Points d’attentions

L’implémentation du Visitor nécessite un certain nombre de classes et d’interfaces. Cela alourdit le code surtout si on a beaucoup de type d’objets. On peut se retrouver avec une explosion de classes qui rendent le code plus difficile à lire et à maintenir.

Le pattern Visitor est parfait pour ajouter de nouveaux traitements sans toucher aux classes existantes. Mais quand on ajoute un nouvel objet visitable, tous les visiteurs doivent être mis à jour pour le rendre visitable. Si on reprend l’exemple de la génération de contenus dynamiques et que l’on souhaite ajouter un nouvel élément (exemple QuoteElement) tous les visiteurs (HTMLRenderer, JSONRenderer, etc…) devront être mis à jour.

Le double dispatch entraîne un appel de méthode supplémentaire. Dans un projet conséquent très orienté performance, le Visitor n’est pas forcément une solution adaptée.

Le design pattern Visitor est très utile dans les langages statiquement typés comme Go, Java ou C++, où le typage strict impose de structurer le code de manière claire. Dans les langages dynamiques comme PHP ou Javascript, utiliser Visitor est souvent inutile ou contre-productif. En PHP par exemple, il est possible d’appeler une méthode sans la définir explicitement.

Au lieu de définir un Visitor, on peut simplement utiliser le duck typing. De cette manière, renderer() peut être appelé sur n’importe quel objet qui possède toHTML() sans avoir besoin d’utiliser une interface.


public function render($element) {
        $method = 'toHTML';
        if (method_exists($element, $method)) {
            return $element->$method();
        }
        throw new Exception("Méthode introuvable");
    }

Dans la même logique, le double dispatch est souvent inutile en langage dynamique. Toujours en PHP, la méthode magique __call() permet d’intercepter dynamiquement les appels de méthodes.


class Element {
    public function __call($name, $arguments) {
        if ($name === 'renderHTML') {
            return "Contenu générique";
        }
    }
}


$element = new Element();
echo $element->renderHTML(); // Appelle dynamiquement la méthode

Implémenter un Visitor

On va définir une interface Visitor qui va “visiter” différents types d’objets. Cette interface regroupe toutes les signatures de fonctions pour les algorithmes à appliquer sur les objets à visiter.

Une classe concrète qui va implémenter l’interface Visitor est créée. Elle centralise l’ensemble des méthodes qui vont appliquer un traitement aux classes visitées.

Une autre interface Visitable est définie et qui donne la signature d’une méthode accept(). Cette interface est implémentée par toutes les classes dites “visitables”.

Exemple d'utilisation de Visitor en Go

Prenons l’exemple d’une validation de formulaire.

Définition de l'interface Visitable


package fieldValidator

// Visitable définit l'interface que chaque champ doit implémenter
type Visitable interface {
   Accept(Visitor)
}

Définition de l'interface Visitor


package fieldValidator

type Visitor interface {
   VisitTextField(*TextField)
   VisitEmailField(*EmailField)
   VisitNumberField(*NumberField)
}

Définition des champs de formulaire


package fieldValidator

type TextField struct {
   Value     string
   MinLength int
}

func (t *TextField) Accept(v Visitor) {
   v.VisitTextField(t)
}

type EmailField struct {
   Email string
}

func (e *EmailField) Accept(v Visitor) {
   v.VisitEmailField(e)
}

type NumberField struct {
   Number int
   Min    int
   Max    int
}

func (n *NumberField) Accept(v Visitor) {
   v.VisitNumberField(n)
}

Implémentation du visiteur de validation


package fieldValidator

import (
   "fmt"
   "regexp"
)

type ValidationVisitor struct {
   Errors []string
}

func (v *ValidationVisitor) VisitTextField(t *TextField) {
   if len(t.Value) < t.MinLength {
      v.Errors = append(v.Errors, fmt.Sprintf("Le texte doit contenir au moins %d caractères.", t.MinLength))
   }
}

func (v *ValidationVisitor) VisitEmailField(e *EmailField) {
   matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, e.Email)
   if !matched {
      v.Errors = append(v.Errors, "L'email n'est pas valide.")
   }
}

func (v *ValidationVisitor) VisitNumberField(n *NumberField) {
   if n.Number < n.Min || n.Number > n.Max {
      v.Errors = append(v.Errors, fmt.Sprintf("Le nombre doit être compris entre %d et %d.", n.Min, n.Max))
   }
}


package main

import (
   "fmt"
   "designPatterns/visitor/fieldValidator"
)

func main() {
   fields := []fieldValidator.Visitable{
      &fieldValidator.TextField{Value: "Hi", MinLength: 5},
      &fieldValidator.EmailField{Email: "invalid-email"},
      &fieldValidator.NumberField{Number: 150, Min: 1, Max: 100},
   }

   validator := &fieldValidator.ValidationVisitor{}

   for _, field := range fields {
      field.Accept(validator)
   }

   if len(validator.Errors) > 0 {
      fmt.Println("Erreurs de validation :")
      for _, err := range validator.Errors {
         fmt.Println("-", err)
      }
   } else {
      fmt.Println("Toutes les données sont valides !")
   }
}

Le même exemple en PHP


namespace Practice\DesignPatterns\Visitor;

interface VisitableInterface
{
   public function accept(VisitorInterface $visitor);
}


namespace Practice\DesignPatterns\Visitor;

interface VisitorInterface
{
   public function visitTextField(TextField $text);

   public function visitEmailField(EmailField $email);

   public function visitNumberField(NumberField $number);
}


namespace Practice\DesignPatterns\Visitor;

class TextField implements VisitableInterface
{
   public string $value;
   public int    $minLength;

   public function __construct(string $value, int $minLength)
   {
       $this->value     = $value;
       $this->minLength = $minLength;
   }

   public function accept(VisitorInterface $visitor)
   {
       $visitor->visitTextField($this);
   }
}


namespace Practice\DesignPatterns\Visitor;

class EmailField implements VisitableInterface
{
   public string $email;

   public function __construct(string $email)
   {
       $this->email = $email;
   }

   public function accept(VisitorInterface $visitor)
   {
       $visitor->visitEmailField($this);
   }
}


namespace Practice\DesignPatterns\Visitor;

class NumberField implements VisitableInterface
{
   public int $number;
   public int $min;
   public int $max;

   public function __construct(int $number, int $min, int $max)
   {
       $this->number = $number;
       $this->min    = $min;
       $this->max    = $max;
   }

   public function accept(VisitorInterface $visitor)
   {
       $visitor->visitNumberField($this);
   }
}


namespace Practice\DesignPatterns\Visitor;

class ValidationVisitor implements VisitorInterface
{
   public array $errors = [];

   public function visitTextField(TextField $text)
   {
       if (strlen($text->value) < $text->minLength) {
           $this->errors[] = "Le texte doit contenir au moins {$text->minLength} caractères.";
       }
   }

   public function visitEmailField(EmailField $email)
   {
       if (!filter_var($email->email, FILTER_VALIDATE_EMAIL)) {
           $this->errors[] = "L'email n'est pas valide.";
       }
   }

   public function visitNumberField(NumberField $number)
   {
       if ($number->number < $number->min || $number->number > $number->max) {
           $this->errors[] = "Le nombre doit être compris entre {$number->min} et {$number->max}.";
       }
   }
}


declare(strict_types=1);

use Practice\DesignPatterns\Visitor\EmailField;
use Practice\DesignPatterns\Visitor\NumberField;
use Practice\DesignPatterns\Visitor\TextField;
use Practice\DesignPatterns\Visitor\ValidationVisitor;

$fields = [
   new TextField("Hi", 5),
   new EmailField("invalid-email"),
   new NumberField(150, 1, 100),
];

$validator = new ValidationVisitor();

foreach ($fields as $field) {
   $field->accept($validator);
}

if (!empty($validator->errors)) {
   echo "Erreurs de validation :\n";
   foreach ($validator->errors as $error) {
       echo "- $error\n";
   }
} else {
   echo "Toutes les données sont valides !\n";
}

Bonus

En PHP, on peut simplifier l’implémentation du pattern Visitor en exploitant les appels de méthodes dynamiques. On garde une interface, mais on évite d’écrire plusieurs classes Visitable. Au lieu d’avoir un TextField, EmailField et NumberField, on gère tout ça avec un attribut $name.


namespace Practice\DesignPatterns\Visitor;

interface VisitableInterface
{
   public function accept(VisitorInterface $visitor);
}


namespace Practice\DesignPatterns\Visitor;

interface VisitorInterface
{
   public function visit(Field $field);
}


namespace Practice\DesignPatterns\Visitor;

class Field implements VisitableInterface
{
   public string $name;
   public mixed  $value;

   public function __construct(string $name, mixed $value)
   {
       $this->name  = $name;
       $this->value = $value;
   }

   public function accept(VisitorInterface $visitor)
   {
       $visitor->visit($this);
   }
}


namespace Practice\DesignPatterns\Visitor;

class ValidationVisitor implements VisitorInterface
{
   private array $rules  = [];
   public array  $errors = [];

   public function __construct()
   {
       $this->rules = [
           'text'   => fn($value) => strlen($value) >= 5 ?: "Le texte doit contenir au moins 5 caractères.",
           'email'  => fn($value) => filter_var($value, FILTER_VALIDATE_EMAIL) ?: "L'email n'est pas valide.",
           'number' => fn($value) => ($value >= 1 && $value <= 100) ?: "Le nombre doit être entre 1 et 100.",
       ];
   }

   public function visit(Field $field)
   {
       $type = $field->name;
       if (isset($this->rules[$type])) {

           $result = $this->rules[$type]($field->value);
           if ($result !== true) {
               $this->errors[] = $result;
           }
       }
   }
}


declare(strict_types=1);

use Practice\DesignPatterns\Visitor\Field;
use Practice\DesignPatterns\Visitor\ValidationVisitor;

$fields = [
   new Field('text', "Hi"), // Trop court
   new Field('email', "invalid-email"), // Invalide
   new Field('number', 150), // Hors limite
];

$validator = new ValidationVisitor();

foreach ($fields as $field) {
   $field->accept($validator);
}

if (!empty($validator->errors)) {
   echo "Erreurs de validation :\n";
   foreach ($validator->errors as $error) {
       echo "- $error\n";
   }
} else {
   echo "Toutes les données sont valides !\n";
}

Conclusion

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern Visitor. Vous pouvez retrouver mon article sur les patrons de conception ou les autres tutoriels sur Adapter, Prototype, Decorator, Strategy et Observer.