Dive Into Design Patterns
Chapitre 8 / 9 · 15 min de lecture

Les patrons comportementaux (1/2)

Répartir les responsabilités et organiser la communication : Chaîne de responsabilité, Commande, Itérateur, Médiateur et Mémento.

Les patrons de création s'occupent de la naissance des objets, les patrons structurels de leur assemblage. Les patrons comportementaux (behavioral patterns), eux, s'intéressent à ce qui se passe ensuite : comment les objets se répartissent les responsabilités et comment ils communiquent. Ils traitent moins de la forme des classes que des algorithmes et des flux de messages qui circulent entre elles.

Le fil rouge de ce chapitre est la lutte contre un même fléau : le couplage qui s'installe quand les objets se parlent en direct, dans tous les sens, sans intermédiaire. Nous verrons cinq réponses complémentaires. La chaîne de responsabilité fait transiter une requête le long d'un pipeline de gestionnaires. La commande transforme une requête en objet manipulable. L'itérateur extrait le parcours d'une collection. Le médiateur centralise les communications désordonnées. Le mémento sauvegarde l'état d'un objet sans briser son encapsulation. Les exemples du livre sont en pseudo-code ; nous les traduisons en TypeScript idiomatique, compilable sous strict.

Chaîne de responsabilité (Chain of Responsibility)

Intention. La chaîne de responsabilité (Chain of Responsibility) fait transiter une requête le long d'une chaîne de gestionnaires (handlers) : chacun décide soit de la traiter, soit de la passer au suivant.

Le problème

Imaginez un système de commande en ligne. Vous voulez d'abord que seuls les utilisateurs authentifiés puissent créer une commande. Vite, d'autres vérifications s'ajoutent et doivent s'exécuter séquentiellement : si l'authentification échoue, inutile d'aller plus loin. Un collègue ajoute une étape de validation pour nettoyer les données brutes. Quelqu'un remarque une faille au bourrage de mots de passe : on ajoute un filtre sur les adresses IP qui échouent à répétition. Un autre propose de mettre en cache les réponses identiques pour accélérer le système.

Le code de ces vérifications, déjà brouillon, enfle à chaque ajout. Modifier une vérification en casse parfois une autre. Pire : pour protéger un autre composant qui ne réclame que certaines vérifications, vous devez dupliquer du code. Le système devient incompréhensible et coûteux à maintenir.

La solution

Comme beaucoup de patrons comportementaux, la chaîne de responsabilité transforme des comportements en objets autonomes appelés gestionnaires. Chaque vérification devient une classe dotée d'une méthode unique. Surtout, le patron suggère de lier ces gestionnaires en chaîne : chacun conserve une référence vers le suivant. En plus de traiter la requête, un gestionnaire peut la passer plus loin — ou décider de stopper net le traitement.

Tous les gestionnaires partagent la même interface ; chacun ne connaît que le suivant. Vous composez ainsi des chaînes à l'exécution, sans coupler votre code aux classes concrètes. C'est exactement ce qui se passe avec les événements d'une interface graphique : un clic remonte du bouton vers ses conteneurs jusqu'à la fenêtre, et le premier élément capable de le gérer le traite.

interface Request {
	user?: unknown;
	key: string;
}
type Response = { error: string };

abstract class Handler {
	private next?: Handler;

	setNext(handler: Handler): Handler {
		this.next = handler;
		return handler; // permet le chaînage fluide
	}

	handle(request: Request): Response | null {
		if (this.next) return this.next.handle(request);
		return null; // fin de chaîne, requête non traitée
	}
}

class AuthHandler extends Handler {
	handle(request: Request): Response | null {
		if (!request.user) return { error: "Non authentifié" };
		return super.handle(request); // on passe au suivant
	}
}

class CacheHandler extends Handler {
	private store = new Map<string, Response>();

	handle(request: Request): Response | null {
		const hit = this.store.get(request.key);
		if (hit) return hit; // on stoppe la chaîne ici
		return super.handle(request);
	}
}

L'analogie de Shvets est limpide : l'appel au support technique. Le répondeur automatique propose neuf solutions inutiles, puis vous passe un opérateur, qui finit par transmettre votre appel à un ingénieur capable, lui, de résoudre votre problème. La requête remonte la chaîne jusqu'au premier maillon compétent.

Astuce

En fournissant un setter pour la référence vers le gestionnaire suivant, vous pouvez insérer, retirer ou réordonner les maillons à l'exécution. C'est ce qui rend ce patron idéal pour les pipelines configurables — les middlewares d'un serveur web en sont l'incarnation la plus connue.

Quand l'utiliser

  • Quand votre programme doit traiter différents types de requêtes de manières variées, sans connaître à l'avance les types ni leur ordre.
  • Quand il est essentiel d'exécuter plusieurs gestionnaires dans un ordre précis.
  • Quand l'ensemble des gestionnaires et leur ordre changent à l'exécution.

Le piège à connaître : certaines requêtes peuvent finir non traitées, en bout de chaîne, sans que personne ne s'en occupe. Prévoyez ce scénario. Notez aussi la parenté avec le décorateur (Decorator) : tous deux reposent sur une composition récursive, mais un décorateur ne doit jamais rompre le flux, alors qu'un maillon de la chaîne le peut à tout moment.

Commande (Command)

Intention. La commande (Command) transforme une requête en objet autonome contenant toutes les informations nécessaires, ce qui permet de la paramétrer, de la mettre en file, de la journaliser et surtout de l'annuler (undo).

Le problème

Vous développez un éditeur de texte avec une barre d'outils. Vous avez une jolie classe Button réutilisable. Mais où placer le code des différents clics ? La solution naïve — créer une sous-classe par bouton — explose vite : une nuée de sous-classes, toutes fragiles dès qu'on touche à Button, et un code d'interface devenu dépendant de la logique métier volatile.

Le pire surgit avec les opérations invoquées de plusieurs endroits. « Copier » peut être déclenché par un bouton de la barre, par le menu contextuel, ou par Ctrl+C. Vous devez alors soit dupliquer le code de l'opération dans plusieurs classes, soit rendre les menus dépendants des boutons — pire encore.

La solution

Le bon design repose souvent sur la séparation des préoccupations : une couche interface et une couche logique métier. La commande suggère que les objets de l'interface n'envoient pas leurs requêtes en direct. À la place, on extrait tous les détails de la requête — l'objet appelé, le nom de la méthode, les arguments — dans une classe commande dotée d'une unique méthode execute().

Les commandes deviennent un intermédiaire commode entre interface et métier. Un bouton ne sait plus quel objet métier recevra la requête : il déclenche simplement sa commande. Comme toutes les commandes partagent la même interface, on peut les interchanger à l'exécution. Les paramètres ? La commande est soit pré-configurée avec, soit capable de les obtenir seule. Plus besoin de sous-classes de boutons : un simple champ command dans Button suffit.

interface Command {
	execute(): boolean; // true si l'état a changé (à historiser)
	undo(): void;
}

class Editor {
	text = "";
	getSelection(): string { return "..."; }
	deleteSelection(): void { /* ... */ }
	replaceSelection(s: string): void { this.text += s; }
}

class CutCommand implements Command {
	private backup = "";
	constructor(private editor: Editor, private app: App) {}

	execute(): boolean {
		this.backup = this.editor.text; // sauvegarde avant action
		this.app.clipboard = this.editor.getSelection();
		this.editor.deleteSelection();
		return true; // l'état a changé : on l'ajoute à l'historique
	}

	undo(): void {
		this.editor.text = this.backup; // on restaure
	}
}

class App {
	clipboard = "";
	private history: Command[] = [];

	run(command: Command): void {
		if (command.execute()) this.history.push(command);
	}

	undo(): void {
		this.history.pop()?.undo(); // dernière commande annulée
	}
}

L'analogie : la commande au restaurant. Le serveur note votre demande sur un papier, le colle au mur de la cuisine ; le papier reste en file d'attente jusqu'à ce que le chef le lise et cuisine. Ce bout de papier contient toutes les informations nécessaires — il découple le client du cuisinier.

À retenir

Le patron commande est sans doute la manière la plus répandue d'implémenter l'annulation/répétition (undo/redo). L'historique est une pile de commandes exécutées, accompagnées de la sauvegarde d'état nécessaire à la restauration. Deux écueils : sauvegarder l'état complet peut être malaisé (l'état est parfois privé — c'est là qu'intervient le mémento) et gourmand en mémoire vive. L'alternative — appliquer l'opération inverse — n'est pas toujours simple, voire impossible.

Quand l'utiliser

  • Quand vous voulez paramétrer des objets avec des opérations, les passer en argument, les stocker, ou les interchanger à l'exécution.
  • Quand vous voulez mettre en file, planifier ou exécuter à distance des opérations (une commande se sérialise).
  • Quand vous voulez des opérations réversibles.

Attention à la sur-ingénierie : la commande ajoute une couche entière entre émetteurs et récepteurs. Pour un simple appel direct sans annulation ni file d'attente, c'est superflu. Ne la confondez pas avec la stratégie (Strategy) : la stratégie décrit différentes façons de faire la même chose ; la commande convertit n'importe quelle opération en objet.

Itérateur (Iterator)

Intention. L'itérateur (Iterator) permet de parcourir séquentiellement les éléments d'une collection sans exposer sa représentation interne (liste, pile, arbre, etc.).

Le problème

Une collection est un conteneur d'objets. La plupart stockent leurs éléments dans de simples listes, mais d'autres reposent sur des piles, arbres, graphes. Peu importe la structure : il faut un moyen de parcourir chaque élément sans repasser sur les mêmes.

Pour une liste, c'est trivial. Mais comment parcourir un arbre ? Un jour vous voulez un parcours en profondeur, le lendemain en largeur, la semaine d'après un accès aléatoire. Empiler ces algorithmes de parcours dans la collection brouille sa responsabilité première — le stockage efficace des données. Et le code client, qui se moque de savoir comment les éléments sont rangés, se retrouve quand même couplé aux classes concrètes de chaque collection.

La solution

L'idée centrale est d'extraire le comportement de parcours dans un objet distinct : l'itérateur. Celui-ci encapsule tous les détails — position courante, nombre d'éléments restants — si bien que plusieurs itérateurs peuvent parcourir la même collection en parallèle, indépendamment.

Tous les itérateurs implémentent la même interface : le code client devient compatible avec n'importe quelle collection ou algorithme de parcours, du moment qu'un itérateur adéquat existe. Besoin d'un nouveau parcours ? Vous créez une nouvelle classe d'itérateur, sans toucher ni à la collection ni au client.

interface Profile {
	email: string;
}
declare function send(to: string, msg: string): void;

interface Iterator<T> {
	hasNext(): boolean;
	next(): T;
}

interface Collection<T> {
	createIterator(): Iterator<T>;
}

class FriendsIterator implements Iterator<Profile> {
	private position = 0;
	constructor(private profiles: Profile[]) {}

	hasNext(): boolean {
		return this.position < this.profiles.length;
	}

	next(): Profile {
		return this.profiles[this.position++];
	}
}

class SocialNetwork implements Collection<Profile> {
	constructor(private friends: Profile[]) {}

	createIterator(): Iterator<Profile> {
		return new FriendsIterator(this.friends);
	}
}

// Le client ne connaît que les interfaces.
function broadcast(it: Iterator<Profile>, message: string): void {
	while (it.hasNext()) {
		const profile = it.next();
		send(profile.email, message);
	}
}

L'analogie : visiter Rome. Vous pouvez errer au hasard, utiliser une application de navigation, ou engager un guide local. Ces trois options agissent comme des itérateurs sur la vaste collection de monuments — chacune offre une façon différente de la parcourir.

Note

Astuce méconnue : passer un itérateur à un client plutôt que la collection entière. Le client ne voit jamais la collection — vous protégez sa structure interne d'actions imprudentes ou malveillantes. JavaScript et TypeScript intègrent d'ailleurs ce patron nativement via le protocole Symbol.iterator et la boucle for...of.

Quand l'utiliser

  • Quand la collection a une structure interne complexe que vous voulez masquer au client.
  • Quand vous voulez réduire la duplication du code de parcours dans l'application.
  • Quand vous voulez parcourir des structures de données variées ou inconnues à l'avance.

Limites honnêtes : pour de simples collections, le patron est de la sur-ingénierie. Et passer par un itérateur peut être moins efficace qu'un parcours direct sur une collection spécialisée.

Médiateur (Mediator)

Intention. Le médiateur (Mediator) réduit les dépendances désordonnées entre objets en faisant passer leurs communications par un objet central, plutôt qu'en direct.

Le problème

Vous avez un formulaire d'édition de profil : champs de texte, cases à cocher, boutons. Certains éléments s'influencent : cocher « J'ai un chien » révèle un champ pour le nom de l'animal ; le bouton d'envoi doit valider tous les champs avant de sauvegarder.

En implantant cette logique directement dans le code des éléments, vous rendez leurs classes irréutilisables ailleurs. La case à cocher est désormais couplée au champ « nom du chien » : impossible de la réutiliser dans un autre formulaire. C'est tout ou rien.

La solution

Le médiateur suggère de cesser toute communication directe entre les composants que vous voulez rendre indépendants. À la place, ils collaborent indirectement via un objet médiateur, qui redirige les appels vers les bons composants. Chaque composant ne dépend plus que d'une seule classe, le médiateur, au lieu d'une dizaine de collègues.

Dans le formulaire, la boîte de dialogue elle-même joue le médiateur — elle connaît déjà ses sous-éléments. Le bouton d'envoi a désormais une unique tâche : notifier la boîte de dialogue du clic. Celle-ci, à réception, effectue les validations ou délègue aux éléments concernés. En extrayant une interface commune pour le médiateur, le bouton fonctionne avec n'importe quelle boîte de dialogue conforme.

interface Mediator {
	notify(sender: Component, event: string): void;
}

abstract class Component {
	constructor(protected mediator: Mediator) {}
}

class Checkbox extends Component {
	checked = false;
	check(): void {
		this.checked = !this.checked;
		this.mediator.notify(this, "check");
	}
}

class TextField extends Component {
	visible = true;
}

// Le médiateur concret encapsule tout le réseau de relations.
class ProfileDialog implements Mediator {
	hasPetCheckbox = new Checkbox(this);
	petNameField = new TextField(this);

	notify(sender: Component, event: string): void {
		if (sender === this.hasPetCheckbox && event === "check") {
			// La logique de coordination vit ICI, pas dans la case.
			this.petNameField.visible = this.hasPetCheckbox.checked;
		}
	}
}

L'analogie est parfaite : la tour de contrôle. Les pilotes ne se parlent pas entre eux pour décider qui atterrit ; toute la communication passe par le contrôleur aérien. Sans lui, chaque pilote devrait connaître tous les avions des environs et négocier avec une nuée de collègues — de quoi faire exploser les statistiques d'accidents.

Attention

Avec le temps, un médiateur peut dégénérer en objet-dieu (God Object) : à force d'y centraliser toutes les relations, il finit par tout savoir et tout faire. Surveillez sa taille ; si la coordination devient trop lourde, envisagez de la scinder. Ne confondez pas non plus le médiateur avec la façade (Facade) : la façade ne fait que simplifier l'accès à un sous-système qui s'ignore d'elle, tandis que le médiateur centralise activement la communication entre composants qui ne se connaissent plus.

Quand l'utiliser

  • Quand certaines classes sont difficiles à modifier car trop couplées à une foule d'autres.
  • Quand vous ne pouvez pas réutiliser un composant ailleurs tant il dépend de ses voisins.
  • Quand vous créez une myriade de sous-classes juste pour réutiliser un comportement de base dans divers contextes.

Mémento (Memento)

Intention. Le mémento (Memento) permet de sauvegarder et restaurer l'état antérieur d'un objet sans révéler les détails de son implémentation — donc sans violer son encapsulation.

Le problème

Reprenons l'éditeur de texte, auquel vous voulez ajouter l'annulation (undo). L'approche directe : avant chaque opération, enregistrer l'état de tous les objets ; à l'annulation, restaurer le dernier instantané.

Mais comment produire cet instantané ? Il faudrait parcourir tous les champs de l'objet et copier leurs valeurs. Or les objets réels cachent leurs données dans des champs privés. Si on les rend tous publics « comme des hippies », on résout le problème immédiat mais on s'expose : refactoriser l'éditeur, ajouter ou retirer un champ, obligera à modifier les classes chargées de copier l'état. Et la classe « instantané », avec ses champs publics miroirs, expose tout l'état de l'éditeur et rend les autres classes dépendantes du moindre détail. Impasse : soit on brise l'encapsulation, soit on rend les instantanés impossibles.

La solution

Tous ces ennuis viennent d'une encapsulation brisée : certains objets envahissent l'espace privé des autres au lieu de les laisser agir. Le mémento délègue la création de l'instantané au véritable propriétaire de l'état — l'objet originateur (originator), qui a un accès total à lui-même.

L'état est stocké dans un objet spécial, le mémento, dont le contenu n'est accessible à aucun autre objet que celui qui l'a produit. Les autres — appelés gardiens (caretakers) — ne manipulent le mémento qu'à travers une interface limitée, donnant accès aux métadonnées (date, nom de l'opération) mais jamais à l'état lui-même. Le gardien peut ainsi empiler les mémentos sans pouvoir les altérer. En TypeScript, l'absence de classes imbriquées se contourne avec une interface intermédiaire opaque :

// Interface limitée : le gardien ne voit QUE les métadonnées.
interface Memento {
	getName(): string;
	getDate(): string;
}

class Editor {
	private text = "";
	private cursorX = 0;

	setText(text: string): void { this.text = text; }
	setCursor(x: number): void { this.cursorX = x; }

	// L'originateur produit un instantané de son propre état.
	save(): Memento {
		return new EditorMemento(this.text, this.cursorX);
	}

	// ... et se restaure lui-même depuis un mémento.
	restore(memento: Memento): void {
		const m = memento as EditorMemento;
		this.text = m.getText();
		this.cursorX = m.getCursorX();
	}
}

// Mémento immuable : aucun setter, état figé à la construction.
class EditorMemento implements Memento {
	private readonly date = new Date().toISOString();
	constructor(
		private readonly text: string,
		private readonly cursorX: number,
	) {}

	getName(): string { return `Sauvegarde ${this.date}`; }
	getDate(): string { return this.date; }
	getText(): string { return this.text; } // réservé à l'éditeur
	getCursorX(): number { return this.cursorX; }
}

// Le gardien empile les mémentos sans jamais lire leur état.
class History {
	private mementos: Memento[] = [];
	constructor(private editor: Editor) {}

	backup(): void { this.mementos.push(this.editor.save()); }
	undo(): void {
		const m = this.mementos.pop();
		if (m) this.editor.restore(m);
	}
}

Le gardien peut être un objet History dédié — ou, comme le note Shvets, une commande, qui capture un mémento juste avant de modifier l'état et le restaure à l'annulation. On combine alors élégamment commande et mémento.

Piège courant

Le mémento peut consommer beaucoup de mémoire vive si les clients créent des instantanés trop souvent ; les gardiens doivent suivre le cycle de vie de l'originateur pour détruire les mémentos obsolètes. Et dans les langages dynamiques comme JavaScript, rien ne garantit réellement que l'état d'un mémento reste inviolé — l'encapsulation y repose sur une convention, pas sur une barrière du compilateur.

Quand l'utiliser

  • Quand vous voulez produire des instantanés d'état pour pouvoir restaurer un état antérieur — annulation, mais aussi transactions réversibles en cas d'erreur.
  • Quand l'accès direct aux champs de l'objet violerait son encapsulation : le mémento rend l'objet seul responsable de son instantané, qu'aucun autre ne peut lire.

Notez l'alternative : le prototype (Prototype) est parfois plus simple, si l'objet à sauvegarder est trivial et sans liens vers des ressources externes.

Tableau récapitulatif

PatronIntentionQuand l'utiliser
Chaîne de responsabilitéFaire transiter une requête le long d'une chaîne de gestionnaires, chacun la traitant ou la passantPipelines de traitements (middlewares, vérifications) dont l'ordre ou la composition varie à l'exécution
CommandeTransformer une requête en objet autonome paramétrable, journalisable, annulableAnnulation/répétition, files d'attente, exécution différée ou distante, découplage interface/métier
ItérateurParcourir une collection sans exposer sa représentation interneStructures complexes (arbres, graphes), parcours multiples ou parallèles, masquage de la structure
MédiateurFaire passer les communications par un objet central plutôt qu'en directComposants trop couplés entre eux, éléments d'interface qui s'influencent, réutilisation de composants
MémentoSauvegarder et restaurer l'état d'un objet sans violer son encapsulationAnnulation, points de sauvegarde, transactions réversibles, état privé à protéger

À retenir

  • Les patrons comportementaux organisent les algorithmes et la communication entre objets ; leur ennemi commun est le couplage direct et désordonné.
  • La chaîne de responsabilité découple émetteur et récepteurs en faisant circuler la requête de maillon en maillon — parfait pour des pipelines configurables, au risque qu'une requête finisse non traitée.
  • La commande convertit une requête en objet : c'est la clé de l'annulation (undo), des files d'attente et de l'exécution différée, au prix d'une couche supplémentaire.
  • L'itérateur extrait le parcours d'une collection, autorise plusieurs parcours parallèles et masque la structure interne ; superflu pour les collections simples.
  • Le médiateur centralise les relations entre composants couplés, mais surveillez la dérive vers l'objet-dieu (God Object).
  • Le mémento délègue l'instantané d'état à son propriétaire, préservant l'encapsulation ; il se marie naturellement avec la commande pour l'undo, en restant vigilant sur la mémoire consommée.