La pile de dossiers sur le bureau était haute, chacun avec une étiquette différente : « CONFIDENTIEL », « URGENT », « ARCHIVE », « CHIFFRÉ ». Un même document pouvait traverser plusieurs couches, acquérant des propriétés au fil de son parcours. Le code, lui, essayait de gérer ça avec un héritage monstrueux : DocumentConfidentielUrgent, DocumentChiffreArchive... Une explosion de classes pour chaque combinaison. Ils avaient besoin d’habiller l’objet, de lui ajouter des comportements à la volée, pas à la naissance. Ils avaient besoin d’un Décorateur.
Extension dynamique
Le pattern Decorator permet d’attacher des responsabilités supplémentaires à un objet de manière dynamique. Il fournit une alternative souple à l’héritage pour étendre les fonctionnalités. Au lieu de créer une sous-classe figée avec une nouvelle combinaison de comportements, on enveloppe l’objet dans un (ou plusieurs) décorateurs qui ajoutent la fonctionnalité souhaitée.
Le principe est celui d’une poupée russe (ou d’un oignon) : un objet cœur est enveloppé par des couches de décorateurs. Chaque couche ajoute sa touche. Le client interagit avec l’objet le plus externe, qui délègue le travail à l’intérieur.
Alternative à l’héritage
L’héritage est statique. Une classe DocumentConfidentielUrgent hérite de Document. Si on veut un document juste confidentiel, ou juste urgent, ou les deux, il faut trois classes. Avec 4 propriétés, c’est 2^4 = 16 classes possibles. C’est l’enfer.
Le Decorator utilise la composition et la délégation. Chaque décorateur implémente la même interface que l’objet qu’il décore, et contient une référence vers cet objet (le composant). Lorsqu’une méthode est appelée sur le décorateur, il fait son travail additionnel, puis délègue l’appel au composant qu’il décore.
Implémentation : Le système de notifications
// --- L'INTERFACE COMMUNE (Component) ---
interface Notificateur {
envoyer(message: string): string; // Retourne une description de l'action
}
// --- L'OBJET DE BASE (Concrete Component) ---
class NotificateurSimple implements Notificateur {
envoyer(message: string): string {
// Envoi basique (ex: dans les logs)
return `Notification de base : "${message}"`;
}
}
// --- LE DÉCORATEUR ABSTRAIT (Decorator) ---
abstract class DecorateurNotificateur implements Notificateur {
// Référence protégée vers le composant décoré.
protected constructor(protected notificateur: Notificateur) {}
// L'implémentation par défaut délègue simplement.
// Les sous-classes concrètes peuvent redéfinir cette méthode.
envoyer(message: string): string {
return this.notificateur.envoyer(message);
}
}
// --- DÉCORATEURS CONCRETS (Concrete Decorators) ---
class DecorateurSMS extends DecorateurNotificateur {
envoyer(message: string): string {
// 1. Fait SON travail spécifique.
const resultatSMS = `[SMS] : "${message}"`;
// 2. Délègue au composant qu'il décore (peut être un autre décorateur ou le composant de base).
const resultatParent = super.envoyer(message);
// 3. Combine les résultats (ou ne fait que son travail avant/après).
return `${resultatParent}\n${resultatSMS}`;
}
}
class DecorateurEmail extends DecorateurNotificateur {
envoyer(message: string): string {
const resultatEmail = `[Email] : "${message}"`;
const resultatParent = super.envoyer(message);
return `${resultatParent}\n${resultatEmail}`;
}
}
class DecorateurUrgent extends DecorateurNotificateur {
envoyer(message: string): string {
// Ajoute une marque d'urgence AVANT de déléguer.
const messageUrgent = `[URGENT] ${message}`;
return super.envoyer(messageUrgent);
}
}
class DecorateurChiffre extends DecorateurNotificateur {
envoyer(message: string): string {
// Simule un chiffrement.
const messageChiffre = this.chiffrer(message);
return super.envoyer(messageChiffre);
}
private chiffrer(texte: string): string {
return btoa(texte); // Chiffrement basique en base64
}
}
// --- UTILISATION : EMPILEMENT DYNAMIQUE ---
function scenarioClient() {
console.log("=== SCÉNARIO 1 : Notification simple ===");
let notif: Notificateur = new NotificateurSimple();
console.log(notif.envoyer("Bonjour"));
// Notification de base : "Bonjour"
console.log("\n=== SCÉNARIO 2 : Notification par SMS et Email ===");
notif = new NotificateurSimple();
notif = new DecorateurSMS(notif); // Enveloppe le simple dans un SMS
notif = new DecorateurEmail(notif); // Enveloppe le résultat dans un Email
console.log(notif.envoyer("Votre commande est prête"));
// Notification de base : "Votre commande est prête"
// [SMS] : "Votre commande est prête"
// [Email] : "Votre commande est prête"
console.log("\n=== SCÉNARIO 3 : Notification Urgente, Chiffrée, par Email ===");
notif = new NotificateurSimple();
notif = new DecorateurUrgent(notif); // Couche 1 : Urgent
notif = new DecorateurChiffre(notif); // Couche 2 : Chiffré
notif = new DecorateurEmail(notif); // Couche 3 : Email
console.log(notif.envoyer("Rapport financier"));
// Notification de base : "[URGENT] Rapport financier" (chiffré en base64)
// [Email] : "UkFQUE9SVCBmaW5hbmNpZXI=" (le message chiffré)
console.log("\n=== SCÉNARIO 4 : Construction en une ligne (plus lisible) ===");
const notifComplexe: Notificateur = new DecorateurEmail(
new DecorateurSMS(
new DecorateurUrgent(
new NotificateurSimple()
)
)
);
console.log(notifComplexe.envoyer("Dernière alerte"));
// Notification de base : "[URGENT] Dernière alerte"
// [SMS] : "[URGENT] Dernière alerte"
// [Email] : "[URGENT] Dernière alerte"
}
scenarioClient();
Diagramme de l’emboîtement (Scénario 3)
Client appelle
↓
+------------------+
| DecorateurEmail | <- Couche externe (Email)
| - notificateur |-----+
+------------------+ |
v
+-------------------+
| DecorateurChiffre | <- Couche intermédiaire (Chiffrement)
| - notificateur |-----+
+-------------------+ |
v
+-------------------+
| DecorateurUrgent | <- Couche intermédiaire (Urgent)
| - notificateur |-----+
+-------------------+ |
v
+-------------------+
| NotificateurSimple| <- Cœur
+-------------------+
Risques de sur-empilement
Le Decorator est puissant, mais dangereux si mal utilisé.
- Complexité de débogage : Une pile de 7 décorateurs est difficile à tracer dans les logs ou un débogueur. Quel décorateur a échoué ? L’ordre des couches est-il correct ?
- Instanciation verbeuse : Construire un objet avec plusieurs décorateurs (
new D3(new D2(new D1(new ComposantBase())))) est illisible. Une factory ou un builder peut aider à créer des configurations prédéfinies. - Interface commune surchargée : Si l’interface du
Componenta 20 méthodes, chaque décorateur doit les implémenter (même si juste pour déléguer). Cela peut générer du code boilerplate. Des langages avec des traits ou mixins peuvent aider. - Identité des objets : Un objet décoré n’est plus du même type que l’objet de base.
instanceof NotificateurSimpleretournerafalsepour un objet décoré. Si le code dépend de l’identité de type, c’est un problème. - Performance : Chaque appel traverse toute la chaîne de délégation. Pour des méthodes appelées des millions de fois, l’overhead peut devenir significatif.
Quand l’utiliser ?
- Quand vous voulez ajouter des responsabilités à des objets individuels de manière transparente (sans affecter les autres objets).
- Quand l’extension par héritage est impraticable (explosion de sous-classes, classe finale dans certaines langues).
- Pour des responsabilités qui peuvent être retirées (on peut théoriquement ne pas appliquer un décorateur).
- Dans les frameworks de middleware (comme Express.js) où chaque couche est un décorateur qui traite la requête et la passe au suivant.
Je rangeai les étiquettes « CONFIDENTIEL » et « URGENT ». Le Decorator était le maître du wrapping. Il ne cassait pas l’objet existant, il l’habillait. C’était du dynamisme pur, de la composition à l’exécution.
Mais comme tout pouvoir, il demandait de la discipline. Trop de couches et le système devenait opaque. Il fallait connaître les risques, surtout celui de l’empilement incontrôlé.
Le prochain pattern serait le Facade. Là où le Decorator ajoutait des couches de complexité, le Facade faisait l’inverse : il cachait une complexité derrière une interface simple. Une seule porte d’entrée pour un sous-système labyrinthique.