La procédure d’interrogatoire standard était affichée au mur : 1) Lecture des droits, 2) Présentation des faits, 3) Questionnement, 4) Synthèse, 5) Signature du procès-verbal. Une trame invariante. Mais l’étape 3, le questionnement, variait selon le suspect : un témoin effrayé, un criminel endurci, un collaborateur. La structure globale était figée, mais une étape clé était laissée ouverte à adaptation. C’était un Template Method. Dans le code, c’était la même chose : un algorithme avec une structure fixe, mais dont certaines parties devaient être personnalisées.
Squelette d’algorithme
Le pattern Template Method définit le squelette d’un algorithme dans une méthode de la classe de base (la classe abstraite ou avec des méthodes hooks), mais délègue certaines étapes de cet algorithme à des sous-classes. Il permet aux sous-classes de redéfinir certaines étapes d’un algorithme sans en changer la structure globale.
La méthode « template » (modèle) est généralement déclarée final (en Java) ou non virtuelle (en C++) pour empêcher les sous-classes de la redéfinir et ainsi de casser l’algorithme. En TypeScript, on simule cela par convention.
Points d’extension
Les étapes que les sous-classes peuvent redéfinir sont appelées méthodes abstraites (ou « opérations primitives ») ou hooks (crochets). Les hooks sont des méthodes concrètes vides ou avec un comportement par défaut, que les sous-classes peuvent optionnellement redéfinir.
Exemple : Génération de rapports (algorithme de génération fixe, format variable)
// Classe abstraite définissant le Template Method
abstract class GenerateurRapport {
// LE TEMPLATE METHOD : Définit le squelette de l'algorithme.
// Elle est finale (par convention) pour empêcher la redéfinition.
public generer(titre: string, donnees: any[]): string {
console.log(`\n📊 Début de la génération du rapport: "${titre}"`);
// 1. Entête (fixe)
const entete = this.genererEntete(titre);
// 2. Corps (variable - méthode abstraite)
const corps = this.genererCorps(donnees);
// 3. Pied de page (fixe, avec un hook optionnel)
const pied = this.genererPied();
// 4. Assemblage (fixe)
const rapportComplet = this.assembler(entete, corps, pied);
// Hook optionnel après génération
this.postTraitement(rapportComplet);
console.log("✅ Génération terminée.");
return rapportComplet;
}
// --- Méthodes concrètes (étapes fixes) ---
private genererEntete(titre: string): string {
const date = new Date().toLocaleDateString();
return `RAPPORT: ${titre}\nDate: ${date}\n${'='.repeat(50)}\n`;
}
private assembler(entete: string, corps: string, pied: string): string {
return entete + corps + '\n' + pied;
}
// --- Méthodes abstraites (points d'extension OBLIGATOIRES) ---
protected abstract genererCorps(donnees: any[]): string;
// --- Hooks (points d'extension OPTIONNELS) ---
protected genererPied(): string {
// Comportement par défaut
return `\n${'-'.repeat(50)}\nFin du rapport.`;
}
protected postTraitement(rapport: string): void {
// Hook vide par défaut. Les sous-classes peuvent l'implémenter si besoin.
// Ex: Envoyer le rapport par email, le logger, etc.
}
}
// Sous-classe concrète pour un rapport HTML
class GenerateurRapportHTML extends GenerateurRapport {
protected genererCorps(donnees: any[]): string {
console.log(" Génération du corps en HTML...");
let html = '<table border="1">\n';
html += ' <tr><th>ID</th><th>Valeur</th></tr>\n';
for (const item of donnees) {
html += ` <tr><td>${item.id}</td><td>${item.valeur}</td></tr>\n`;
}
html += '</table>';
return html;
}
// Redéfinition du hook pour ajouter des balises HTML autour
protected genererPied(): string {
const piedParDefaut = super.genererPied(); // On peut appeler l'implémentation parente
return `<footer>\n${piedParDefaut}\n</footer>`;
}
protected postTraitement(rapport: string): void {
console.log(" (Hook HTML) Rapport HTML prêt. Peut être servi par un serveur web.");
}
}
// Sous-classe concrète pour un rapport texte simple
class GenerateurRapportTexte extends GenerateurRapport {
protected genererCorps(donnees: any[]): string {
console.log(" Génération du corps en texte brut...");
let texte = '';
for (const item of donnees) {
texte += ` - ${item.id}: ${item.valeur}\n`;
}
return texte;
}
// On ne redéfinit pas genererPied(), on garde le comportement par défaut.
// On ne redéfinit pas postTraitement().
}
// Sous-classe pour un rapport CSV
class GenerateurRapportCSV extends GenerateurRapport {
protected genererCorps(donnees: any[]): string {
console.log(" Génération du corps en CSV...");
let csv = 'ID,Valeur\n';
for (const item of donnees) {
csv += `${item.id},${item.valeur}\n`;
}
return csv;
}
protected genererPied(): string {
return ''; // Le CSV n'a pas de pied de page
}
protected postTraitement(rapport: string): void {
console.log(" (Hook CSV) Vérification de la syntaxe CSV...");
// Simulation d'une validation
if (rapport.includes(',,')) {
console.warn(" Attention: cellule vide détectée.");
}
}
}
// --- Scénario d'utilisation ---
console.log("=== GÉNÉRATEUR DE RAPPORTS (Template Method) ===\n");
const donnees = [
{ id: 1, valeur: 'Observation A' },
{ id: 2, valeur: 'Observation B' },
{ id: 3, valeur: 'Observation C' }
];
// Génération HTML
const htmlGenerator = new GenerateurRapportHTML();
const rapportHTML = htmlGenerator.generer('Enquête quotidienne', donnees);
console.log("\n--- Rapport HTML (extrait) ---");
console.log(rapportHTML.substring(0, 150) + "...");
// Génération Texte
const texteGenerator = new GenerateurRapportTexte();
const rapportTexte = texteGenerator.generer('Enquête quotidienne', donnees);
console.log("\n--- Rapport Texte ---");
console.log(rapportTexte);
// Génération CSV
const csvGenerator = new GenerateurRapportCSV();
const rapportCSV = csvGenerator.generer('Données export', donnees);
console.log("\n--- Rapport CSV ---");
console.log(rapportCSV);
Limites de l’héritage
Le Template Method est un pattern de classe (portée classe, pas objet). Il repose sur l’héritage. Cela pose plusieurs problèmes, caractéristiques des limites de l’héritage comme mécanisme de réutilisation :
- Rigidité : L’algorithme est figé dans la hiérarchie de classes. Si on veut une variante qui change l’ordre des étapes (ex: pied de page avant le corps), il faut créer une nouvelle classe de base ou dupliquer du code.
- Couplage fort : Les sous-classes sont étroitement couplées à la classe de base. Elles dépendent de son implémentation (même si juste du squelette). Changer la classe de base (ajouter une nouvelle étape abstraite) brise toutes les sous-classes.
- Explosion hiérarchique : Si on a plusieurs dimensions de variation (format et type de rapport et mode de livraison), on peut se retrouver avec une explosion de sous-classes (
RapportHTMLPourEmail,RapportCSVPourFichier, etc.). C’est le problème de l’héritage unique. - Violation du Principe de Substitution de Liskov (LSP) : Si une sous-classe redéfinit une méthode concrète de la classe de base (même si ce n’est pas interdit), elle peut casser le flux de l’algorithme. Le pattern repose sur la confiance que les sous-classes respecteront le contrat.
- Difficile à composer : On ne peut pas facilement composer deux variations (ex: prendre le corps du
GenerateurRapportHTMLet le pied duGenerateurRapportCSV) sans créer une nouvelle sous-classe.
Quand l’utiliser malgré tout ?
- Quand on a un algorithme dont la structure est vraiment stable et invariable, et dont seules certaines étapes précises varient.
- Quand on veut contrôler les extensions : le Template Method donne un cadre strict. Les sous-classes ne peuvent modifier que ce qui est prévu.
- Dans les frameworks : C’est un pattern très utilisé dans les frameworks qui fournissent un flux de contrôle général, et laissent l’application remplir les blancs (ex: cycle de vie d’un composant UI, traitement d’une requête HTTP).
Alternative : Strategy avec composition
Beaucoup des limites du Template Method sont résolues par le pattern Strategy, qui utilise la composition au lieu de l’héritage. Au lieu d’avoir une méthode template dans une classe de base, on a une classe contexte qui délègue chaque étape variable à un objet stratégie. Cela offre plus de flexibilité (on peut changer les stratégies à la volée, les composer), mais au prix d’une structure moins « cadrante ».
Le choix entre Template Method et Strategy est souvent un choix entre héritage (cadre rigide mais simple) et composition (flexible mais plus complexe à configurer).
La procédure d’interrogatoire restait sur le mur, immuable. Le Template Method était le gardien de la structure. Il garantissait que certaines étapes seraient toujours exécutées dans le bon ordre, tout en permettant une adaptation contrôlée.
C’était un pattern « paternaliste » : la classe de base sait ce qui est bon pour ses enfants. Efficace dans un cadre bien défini, étouffant si le besoin d’évolution dépasse le cadre prévu.
Le prochain pattern, Visitor, serait d’une nature totalement différente : il sépare un algorithme de la structure d’objets sur laquelle il opère. Un outil puissant mais complexe, souvent réservé à des cas très spécifiques.