L’expert médico-légal arrivait sur la scène de crime. Il avait ses protocoles : examiner une arme, analyser une tache de sang, relever une empreinte. Chaque type de preuve nécessitait une expertise spécifique. La collection de preuves (arme, sang, empreinte) était fixe, mais de nouveaux protocoles d’analyse pouvaient apparaître (analyse ADN, reconstruction 3D). On ne pouvait pas modifier les classes de preuves à chaque nouvelle technique. Il fallait un système où l’expert « visite » chaque preuve et y applique son savoir-faire. Il fallait un Visitor.
Ajouter des comportements
Le pattern Visitor permet de séparer un algorithme de la structure d’objets sur laquelle il opère. Il est utilisé quand on a une structure d’objets stable (une hiérarchie de classes comme Arme, Sang, Empreinte), mais qu’on veut pouvoir ajouter de nouvelles opérations sur ces objets sans modifier leurs classes.
Sans Visitor, on serait tenté d’ajouter une méthode analyser() à chaque classe de preuve. Mais cela viole le principe Ouvert/Fermé (OCP) : on ouvre les classes à chaque nouvelle opération. Avec Visitor, on crée une nouvelle classe de visiteur pour chaque nouvelle opération (ex: VisiteurAnalyser, VisiteurEnregistrer, VisiteurExporter). Le visiteur « visite » chaque élément de la structure et exécute l’opération appropriée.
Double dispatch
Le cœur technique du Visitor est le double dispatch (double envoi). Dans la plupart des langages OO (comme TypeScript), le polymorphisme est simple dispatch : la méthode exécutée dépend du type de l’objet receveur. Le double dispatch fait dépendre la méthode exécutée à la fois du type du visiteur et du type de l’élément visité.
On l’implémente ainsi : chaque élément accepte un visiteur en appelant une méthode accept(visiteur: Visitor). À l’intérieur de cette méthode, l’élément appelle une méthode spécifique sur le visiteur, en se passant lui-même (visiteur.visiterArme(this)). Ainsi, le visiteur sait exactement quel type d’élément il visite.
Exemple : Analyse de preuves forensiques
// --- HIÉRARCHIE DES ÉLÉMENTS (Structure stable) ---
interface Preuve {
// Méthode d'acceptation du double dispatch
accept(visiteur: VisiteurPreuve): void;
}
class Arme implements Preuve {
constructor(public type: string, public calibre: number) {}
accept(visiteur: VisiteurPreuve): void {
// "Je suis une Arme, appelle la méthode qui me concerne sur le visiteur."
visiteur.visiterArme(this);
}
}
class TacheSang implements Preuve {
constructor(public groupeSanguin: string, public localisation: string) {}
accept(visiteur: VisiteurPreuve): void {
visiteur.visiterTacheSang(this);
}
}
class EmpreinteDigitale implements Preuve {
constructor(public doigt: string, public qualite: number) {}
accept(visiteur: VisiteurPreuve): void {
visiteur.visiterEmpreinteDigitale(this);
}
}
// --- INTERFACE DU VISITEUR (Définit une opération par type d'élément) ---
interface VisiteurPreuve {
visiterArme(arme: Arme): void;
visiterTacheSang(sang: TacheSang): void;
visiterEmpreinteDigitale(empreinte: EmpreinteDigitale): void;
}
// --- VISITEURS CONCRETS (Nouvelles opérations sans toucher aux éléments) ---
class VisiteurAnalyser implements VisiteurPreuve {
visiterArme(arme: Arme): void {
console.log(`🔫 Analyse de l'arme: ${arme.type} calibre ${arme.calibre}`);
console.log(` → Vérification des résidus de tir, numéro de série...`);
// Logique complexe d'analyse d'arme
}
visiterTacheSang(sang: TacheSang): void {
console.log(`🩸 Analyse de la tache de sang: groupe ${sang.groupeSanguin}`);
console.log(` → Extraction ADN, détermination de l'angle d'impact...`);
}
visiterEmpreinteDigitale(empreinte: EmpreinteDigitale): void {
console.log(`🖐️ Analyse de l'empreinte: doigt ${empreinte.doigt}, qualité ${empreinte.qualite}/10`);
console.log(` → Comparaison avec la base AFIS...`);
}
}
class VisiteurEnregistrer implements VisiteurPreuve {
private registre: string[] = [];
visiterArme(arme: Arme): void {
const entree = `ARME|${arme.type}|${arme.calibre}`;
this.registre.push(entree);
console.log(`📝 Enregistrement de l'arme dans le registre: ${entree}`);
}
visiterTacheSang(sang: TacheSang): void {
const entree = `SANG|${sang.groupeSanguin}|${sang.localisation}`;
this.registre.push(entree);
console.log(`📝 Enregistrement du sang: ${entree}`);
}
visiterEmpreinteDigitale(empreinte: EmpreinteDigitale): void {
const entree = `EMPREINTE|${empreinte.doigt}|${empreinte.qualite}`;
this.registre.push(entree);
console.log(`📝 Enregistrement de l'empreinte: ${entree}`);
}
getRegistre(): string[] {
return this.registre;
}
}
class VisiteurGenererRapportHTML implements VisiteurPreuve {
private contenu: string = '';
visiterArme(arme: Arme): void {
this.contenu += `<section class="arme"><h2>Arme</h2><p>Type: ${arme.type}, Calibre: ${arme.calibre}</p></section>`;
}
visiterTacheSang(sang: TacheSang): void {
this.contenu += `<section class="sang"><h2>Tache de sang</h2><p>Groupe: ${sang.groupeSanguin}, Localisation: ${sang.localisation}</p></section>`;
}
visiterEmpreinteDigitale(empreinte: EmpreinteDigitale): void {
this.contenu += `<section class="empreinte"><h2>Empreinte digitale</h2><p>Doigt: ${empreinte.doigt}, Qualité: ${empreinte.qualite}</p></section>`;
}
getRapport(): string {
return `<html><body>${this.contenu}</body></html>`;
}
}
// --- STRUCTURE DES OBJETS (Collection de preuves) ---
class SceneDeCrime {
private preuves: Preuve[] = [];
ajouterPreuve(preuve: Preuve): void {
this.preuves.push(preuve);
}
// Méthode qui permet à un visiteur de "visiter" toute la scène
accepterVisiteur(visiteur: VisiteurPreuve): void {
console.log(`\n=== Visite de la scène de crime par ${visiteur.constructor.name} ===`);
for (const preuve of this.preuves) {
preuve.accept(visiteur); // Double dispatch ici
}
}
}
// --- SCÉNARIO ---
console.log("=== SYSTÈME FORENSIQUE (Visitor Pattern) ===\n");
// 1. Construction de la scène de crime (structure stable)
const scene = new SceneDeCrime();
scene.ajouterPreuve(new Arme('Revolver', 38));
scene.ajouterPreuve(new TacheSang('O+', 'Mur nord'));
scene.ajouterPreuve(new EmpreinteDigitale('Index droit', 8));
scene.ajouterPreuve(new Arme('Couteau', 0));
// 2. Création des visiteurs (nouvelles opérations)
const analyseur = new VisiteurAnalyser();
const archiviste = new VisiteurEnregistrer();
const rapporteur = new VisiteurGenererRapportHTML();
// 3. Application des différentes opérations sur la même structure
scene.accepterVisiteur(analyseur);
scene.accepterVisiteur(archiviste);
scene.accepterVisiteur(rapporteur);
console.log("\n--- Résultat de l'enregistrement ---");
console.log(archiviste.getRegistre());
console.log("\n--- Extrait du rapport HTML ---");
console.log(rapporteur.getRapport().substring(0, 200) + "...");
// 4. AJOUT FACILE D'UNE NOUVELLE OPÉRATION (sans modifier Arme, Sang, etc.)
console.log("\n=== Ajout d'un nouveau visiteur (Export JSON) ===");
class VisiteurExportJSON implements VisiteurPreuve {
private items: any[] = [];
visiterArme(arme: Arme): void {
this.items.push({ type: 'arme', data: { armeType: arme.type, calibre: arme.calibre } });
}
visiterTacheSang(sang: TacheSang): void {
this.items.push({ type: 'sang', data: { groupe: sang.groupeSanguin, localisation: sang.localisation } });
}
visiterEmpreinteDigitale(empreinte: EmpreinteDigitale): void {
this.items.push({ type: 'empreinte', data: { doigt: empreinte.doigt, qualite: empreinte.qualite } });
}
getJSON(): string {
return JSON.stringify({ preuves: this.items }, null, 2);
}
}
const exporteur = new VisiteurExportJSON();
scene.accepterVisiteur(exporteur);
console.log(exporteur.getJSON());
Coût de rigidité
Le Visitor n’est pas gratuit. Il comporte des coûts et des rigidités importants :
- Violation du Principe Ouvert/Fermé (OCP) pour les éléments : Si on ajoute un nouveau type d’élément à la hiérarchie (ex: une nouvelle classe
FibreTextile), on doit modifier tous les visiteurs existants pour ajouter une méthodevisiterFibreTextile(). C’est l’inverse du bénéfice promis ! Visitor est donc idéal quand la hiérarchie d’éléments est stable, mais que les opérations sont fréquemment ajoutées. - Couplage entre visiteurs et éléments : Les visiteurs connaissent l’intimité de chaque classe d’élément (leurs attributs). Cela crée un couplage fort.
- Difficile d’ajouter un état au parcours : Si l’opération a besoin d’un état global qui évolue pendant la visite (ex: un compteur), le visiteur doit le gérer lui-même.
- Accès restreint aux membres privés : Dans certains langages, le visiteur peut avoir besoin d’un accès spécial (friend, package) aux attributs privés des éléments. En TypeScript, on peut utiliser
publicou des accesseurs. - Complexité : Le pattern ajoute beaucoup de code boilerplate (les méthodes
acceptdans chaque élément, l’interface du visiteur avec une méthode par type).
Quand l’utiliser ?
- Quand on a une hiérarchie de classes stable (les éléments changent rarement).
- Quand on a beaucoup d’opérations différentes à appliquer sur ces éléments, et qu’on veut les garder séparées.
- Quand les opérations ont besoin d’accéder à presque tous les détails des éléments.
- Quand la structure des éléments est complexe (comme un AST – Abstract Syntax Tree – dans un compilateur), et qu’on veut de nombreux traitements (interprétation, optimisation, génération de code, formatting).
Alternative : Pattern Matching
Dans les langages fonctionnels ou ceux supportant le pattern matching (comme Scala, Rust, ou bientôt TypeScript avec les discriminated unions avancés), on peut souvent se passer du Visitor. On a une structure de données algébrique (les preuves) et des fonctions qui font du pattern matching sur le type de preuve. C’est plus simple et plus élégant, mais moins « objet ».
L’expert médico-légal rangeait son matériel. Le Visitor lui avait permis d’appliquer ses nouveaux protocoles sans toucher aux preuves elles-mêmes. C’était un pattern lourd, spécialisé, qui résolvait un problème précis : séparer radicalement les algorithmes des structures sur lesquelles ils opèrent.
Avec le Visitor, on terminait le tour d’horizon des principaux patterns GoF. Il en restait quelques-uns (Iterator, Memento, Interpreter) souvent plus spécialisés ou intégrés dans les langages.
Le prochain sera le pattern Iterator, tellement fondamental qu’il est natif dans les boucles for...of de JavaScript.