Command, Active Object, Template Method & Strategy
Encapsuler une requête en objet (Command, Active Object), et deux façons de réutiliser un algorithme : héritage (Template Method) ou délégation (Strategy).
Nous abordons ici les premiers patrons de la grande étude de cas du livre : la conception d'un système de paie par lots (Payroll). Plutôt que de présenter les patrons comme un catalogue abstrait, Martin les fait émerger dans du vrai code, là où un problème concret les appelle. Avant même d'écrire la paie, deux familles de patrons s'imposent : ceux qui encapsulent une action dans un objet (Command, Active Object) et ceux qui séparent un algorithme générique de ses détails (Template Method, Strategy). Ce chapitre couvre ces quatre patrons, en transposant en TypeScript idiomatique les exemples canoniques du livre — copieur, base d'employés, dactylographe différé, tri à bulles.
Note
Le système de paie sert de fil rouge à toute cette section du livre. Le cahier des charges distingue trois types d'employés (horaires, salariés, commissionnés), plusieurs méthodes de paiement (courrier, retrait, virement) et des retenues syndicales. C'est ce contexte qui motivera, chapitre après chapitre, l'apparition de Command, Template Method, Strategy, Singleton, Null Object, Factory et Facade.
Command : encapsuler une requête dans un objet
De tous les patrons de conception, Command est l'un des plus simples et des plus élégants — et sa simplicité est trompeuse. Le patron se résume à une interface dotée d'une seule méthode :
interface Command {
execute(): void;
} Il semble absurde qu'un patron puisse se réduire à si peu. Pourtant, une ligne très intéressante vient d'être franchie. La plupart des classes associent un ensemble de méthodes à un ensemble de variables. Command ne fait pas cela : il encapsule une fonction, dépourvue de toute variable. En termes strictement orientés objet, c'est presque un blasphème — cela élève le rôle d'une fonction au rang de classe, cela sent la décomposition fonctionnelle. C'est précisément à cette frontière où deux paradigmes s'entrechoquent que des choses intéressantes se produisent.
Commandes simples : piloter du matériel
Martin raconte une mission de conseil chez un fabricant de photocopieurs, pour le logiciel embarqué temps réel qui pilotait les organes de la machine. L'équipe a utilisé Command pour contrôler les périphériques matériels, en créant une hiérarchie de commandes simples :
class RelayOnCommand implements Command {
constructor(private readonly adresse: number) {}
execute(): void {
// active le relais situé à `adresse`
}
}
class MotorOffCommand implements Command {
constructor(private readonly adresse: number) {}
execute(): void {
// coupe le moteur situé à `adresse`
}
} Quand on appelle execute() sur un RelayOnCommand, il enclenche un certain relais ; l'adresse du relais ou du moteur lui est passée au constructeur. Une fois cette structure en place, on peut faire circuler des objets Command dans tout le système et les déclencher sans rien savoir du type concret qu'ils représentent.
Le système était piloté par événements : relais, moteurs et embrayages changeaient d'état au gré de signaux captés par des senseurs. Quand un capteur optique détectait qu'une feuille avait atteint un certain point du chemin papier, il fallait engager un embrayage donné. La solution : lier la ClutchOnCommand appropriée à l'objet qui surveillait ce capteur.
Sensor ──(détecte un événement)──► appelle execute() sur ──► Command L'avantage est considérable. Le Sensor n'a aucune idée de ce qu'il fait : dès qu'il détecte un événement, il appelle simplement execute() sur la commande à laquelle il est lié. Les capteurs n'ont plus à connaître ni les embrayages, ni les relais, ni la structure mécanique du chemin papier. Toute la complexité — déterminer quel relais fermer quand tel capteur signale — migre vers une fonction d'initialisation qui « câble » les liaisons en un seul endroit. On pourrait même décrire ce câblage dans un fichier texte lu au démarrage : la configuration du système devient ajustable sans recompilation.
Transactions : valider puis exécuter
Une autre utilisation classique de Command, et qui servira directement à la paie, concerne les transactions. Imaginons un logiciel maintenant une base d'employés : on peut ajouter, supprimer ou modifier des employés. Une commande de transaction sert alors de dépôt pour les données non validées, implémente la validation, puis l'exécution.
interface Transaction {
validate(): boolean;
execute(): void;
}
class AddEmployeeTransaction implements Transaction {
constructor(
private readonly name: string,
private readonly address: string,
private readonly classification: PayClassification,
) {}
validate(): boolean {
// vérifie la cohérence syntaxique et sémantique des données :
// par exemple, qu'aucun employé identique n'existe déjà
return this.name.length > 0 && this.address.length > 0;
}
execute(): void {
// crée l'Employee à partir des champs validés et l'insère en base
}
} La méthode validate examine toutes les données et s'assure qu'elles ont du sens — y compris en vérifiant leur cohérence avec l'état courant de la base. La méthode execute utilise ensuite les données validées pour mettre à jour la base.
Découplage physique et temporel
Ce découpage offre deux bénéfices distincts.
Le découplage physique d'abord : on s'attend à ce que les données d'un nouvel employé proviennent d'une boîte de dialogue dans une interface graphique. Il serait désastreux que le code de l'IHM contienne lui-même la validation et l'exécution — un tel couplage interdirait de réutiliser cette logique avec d'autres interfaces. En isolant validation et exécution dans AddEmployeeTransaction, on les détache de l'interface de saisie, et l'on sépare aussi le code qui manipule la base des entités métier elles-mêmes.
Le découplage temporel ensuite : une fois les données saisies, rien n'oblige à appeler validate et execute immédiatement. Les objets transaction peuvent être conservés dans une liste, validés sur-le-champ, puis exécutés bien plus tard. Supposons qu'une base ne puisse changer qu'entre minuit et une heure du matin : au lieu d'attendre minuit pour tout saisir en catastrophe, on saisit et valide les commandes dans la journée, et on les exécute la nuit venue. Command nous donne cette capacité gratuitement.
Astuce
Dès qu'une action doit être différée, mise en file, rejouée ou journalisée, songez à Command. Transformer une requête en objet, c'est pouvoir la stocker, la transmettre et l'exécuter quand bon vous semble — au lieu de l'appeler ici et maintenant.
Undo : la commande qui sait se défaire
Il suffit d'ajouter une méthode undo() à l'interface. Si execute() peut être implémentée pour mémoriser les détails de l'opération qu'elle réalise, alors undo() peut s'appuyer sur cette mémoire pour annuler l'opération et restaurer l'état initial.
interface Command {
execute(): void;
undo(): void;
}
class DrawCircleCommand implements Command {
private idCercleCree: number | null = null;
execute(): void {
// suit la souris, dessine le cercle, l'ajoute au canvas
// et MÉMORISE l'identifiant du cercle créé
this.idCercleCree = canvas.ajouterCercle(/* ... */);
}
undo(): void {
// supprime du canvas le cercle mémorisé lors de execute()
if (this.idCercleCree !== null) canvas.supprimerForme(this.idCercleCree);
}
} Le scénario : l'utilisateur clique sur « dessiner un cercle », le système crée un DrawCircleCommand et l'exécute ; la commande suit la souris, ajoute le cercle au canvas et retient son identifiant dans une variable privée. Le système empile alors la commande consommée sur une pile de commandes terminées. Plus tard, un clic sur « annuler » dépile cette commande et appelle undo() dessus, qui supprime le cercle mémorisé. La vertu de cette technique : le code qui sait défaire une commande est toujours juste à côté du code qui sait la faire. On implémente ainsi l'annulation dans presque n'importe quelle application.
Active Object : un multitâche simple bâti sur Command
L'une des utilisations favorites de Martin est le patron Active Object, une très vieille technique pour implémenter plusieurs fils d'exécution. Elle a fourni un noyau multitâche minimal à des milliers de systèmes industriels. L'idée est d'une grande simplicité : un moteur maintient une liste chaînée de commandes ; on peut y ajouter des commandes, ou appeler run(), qui parcourt la liste en exécutant et en retirant chaque commande.
interface Command {
execute(): void;
}
class ActiveObjectEngine {
private commands: Command[] = [];
addCommand(c: Command): void {
this.commands.push(c);
}
run(): void {
while (this.commands.length > 0) {
const c = this.commands.shift()!;
c.execute();
}
}
} Cela paraît anodin. Mais imaginez qu'une des commandes, en s'exécutant, se remette elle-même dans la liste. La liste ne se viderait jamais et run() ne reviendrait jamais. C'est exactement ce mécanisme qui fait naître le multitâche.
SleepCommand : attendre sans bloquer
La SleepCommand reçoit trois éléments : un délai en millisecondes, le moteur dans lequel elle tourne, et une commande wakeup à exécuter une fois le délai écoulé. À chaque exécution, elle vérifie si le délai est passé. Sinon, elle se réinsère dans le moteur ; sinon, elle y insère la commande de réveil.
class SleepCommand implements Command {
private started = false;
private startTime = 0;
constructor(
private readonly sleepTime: number,
private readonly engine: ActiveObjectEngine,
private readonly wakeupCommand: Command,
) {}
execute(): void {
const now = Date.now();
if (!this.started) {
this.started = true;
this.startTime = now;
this.engine.addCommand(this); // se remet en file
} else if (now - this.startTime < this.sleepTime) {
this.engine.addCommand(this); // pas encore l'heure : encore en file
} else {
this.engine.addCommand(this.wakeupCommand); // réveil !
}
}
} L'analogie avec un programme multithread attendant un événement est éclairante. Dans un système classique, un thread en attente invoque un appel système qui le bloque jusqu'à l'événement. Ici, le programme ne bloque jamais : tant que la condition n'est pas remplie, la commande se replace simplement dans le moteur. On parle de tâches RTC (Run-To-Completion) : chaque commande s'exécute jusqu'au bout avant que la suivante ne démarre, parce qu'aucune ne bloque. Avantage notable : toutes les tâches RTC partagent la même pile d'exécution, sans qu'il faille allouer une pile distincte par thread — précieux dans les systèmes à mémoire contrainte avec de nombreuses tâches.
DelayedTyper : du comportement multitâche
Voici un programme complet qui exhibe un vrai comportement concurrent. Un DelayedTyper imprime un caractère, vérifie un drapeau d'arrêt et, s'il n'est pas levé, se réarme via une SleepCommand.
class DelayedTyper implements Command {
private static engine = new ActiveObjectEngine();
private static stop = false;
constructor(
private readonly delay: number,
private readonly char: string,
) {}
execute(): void {
process.stdout.write(this.char);
if (!DelayedTyper.stop) this.delayAndRepeat();
}
private delayAndRepeat(): void {
DelayedTyper.engine.addCommand(
new SleepCommand(this.delay, DelayedTyper.engine, this),
);
}
static main(): void {
this.engine.addCommand(new DelayedTyper(100, "1"));
this.engine.addCommand(new DelayedTyper(300, "3"));
this.engine.addCommand(new DelayedTyper(500, "5"));
this.engine.addCommand(new DelayedTyper(700, "7"));
const stopCommand: Command = { execute: () => (DelayedTyper.stop = true) };
this.engine.addCommand(new SleepCommand(20000, this.engine, stopCommand));
this.engine.run();
}
} Chaque DelayedTyper boucle indéfiniment, imprimant son caractère et attendant son délai, jusqu'à ce que le drapeau stop soit levé par une SleepCommand finale. L'exécution produit une chaîne de 1, 3, 5 et 7 entrelacés — et deux exécutions successives donnent des chaînes différentes, car l'horloge processeur et l'horloge temps réel ne sont jamais parfaitement synchrones.
135711311511371113151131715131113151731111351113711531111357...
1357111315131711315113117135111311517 31113151131711351113117... Ce comportement non déterministe est la marque de fabrique des systèmes multitâches — et la source de bien des souffrances. Quiconque a débogué de l'embarqué temps réel sait combien le non-déterminisme est rétif au débogage.
À retenir
La simplicité de Command dissimule sa polyvalence : transactions de base de données, contrôle de périphériques, noyaux multitâches, administration faire/défaire des interfaces graphiques. On lui reproche de briser le paradigme objet en privilégiant la fonction sur la classe. C'est peut-être vrai — mais dans le monde réel du développeur, Command rend d'immenses services.
Template Method & Strategy : héritage contre délégation
Au début des années 90, l'orienté objet était grisé par l'héritage : programmer par différence, prendre une classe presque utile et n'en redéfinir que ce qui dérange, bâtir des taxonomies entières où chaque niveau réutilise le code des niveaux supérieurs. Le rêve s'est révélé trop naïf. Dès 1995, il était clair que l'héritage était facile à surutiliser, et coûteux quand on en abuse. C'est de cette époque que date l'adage du Gang des Quatre : « Préférez la composition d'objets à l'héritage de classe. »
Les deux patrons qui suivent incarnent cette tension. Template Method et Strategy résolvent le même problème — séparer un algorithme générique de son contexte détaillé — mais Template Method le fait par héritage, et Strategy par délégation. Tous deux servent le principe d'inversion des dépendances (Dependency-Inversion Principle, DIP) : on veut que l'algorithme générique ne dépende pas de l'implémentation détaillée, mais que les deux dépendent d'abstractions.
Template Method : le squelette dans la classe de base
Considérez la structure de boucle principale que l'on retrouve dans une multitude de programmes : initialiser, boucler tant qu'on n'a pas fini, puis nettoyer. Template Method place tout le code générique dans une méthode implémentée d'une classe de base abstraite, en déléguant chaque détail à des méthodes abstraites.
abstract class Application {
private isDone = false;
protected abstract init(): void;
protected abstract idle(): void;
protected abstract cleanup(): void;
protected setDone(): void {
this.isDone = true;
}
protected done(): boolean {
return this.isDone;
}
// La « méthode template » : elle capture l'algorithme générique
// et diffère les détails aux méthodes abstraites.
run(): void {
this.init();
while (!this.done()) this.idle();
this.cleanup();
}
} La méthode run contient la structure figée de l'algorithme ; tout le travail est différé à init, idle et cleanup. Pour écrire un programme concret — une conversion de Fahrenheit en Celsius lue sur l'entrée standard — il suffit d'hériter et de remplir les trous :
class FtoCTemplateMethod extends Application {
protected init(): void {
// ouvre l'entrée standard
}
protected idle(): void {
const ligne = lireLigne();
if (ligne === null || ligne.length === 0) {
this.setDone();
} else {
const fahr = parseFloat(ligne);
const celcius = (5.0 / 9.0) * (fahr - 32);
console.log(`F=${fahr}, C=${celcius}`);
}
}
protected cleanup(): void {
console.log("ftoc exit");
}
} Attention
Abus de patron ! Encapsuler la boucle principale de toute application de l'univers semblait merveilleux, mais appliquer Template Method à un programme aussi trivial est ridicule : on l'a compliqué et alourdi sans rien y gagner. Les patrons sont de beaux outils, mais leur existence ne signifie pas qu'il faille toujours les employer. Ici, le patron était applicable, mais son coût dépassait son bénéfice.
Tri à bulles : un exemple plus utile
Reprenons un tri à bulles écrit « en dur » sur des entiers : la méthode sort connaît l'algorithme, et deux méthodes auxiliaires, swap et compareAndSwap, gèrent les détails des entiers et du tableau. Avec Template Method, on extrait l'algorithme dans une classe de base abstraite qui appelle deux méthodes abstraites, outOfOrder (les deux éléments adjacents sont-ils en désordre ?) et swap (échange deux cellules).
abstract class BubbleSorter {
protected length = 0;
private operations = 0;
protected abstract swap(index: number): void;
protected abstract outOfOrder(index: number): boolean;
// L'algorithme générique : il ignore tout du tableau
// et du type d'objets qu'il contient.
protected doSort(): number {
this.operations = 0;
if (this.length <= 1) return this.operations;
for (let nextToLast = this.length - 2; nextToLast >= 0; nextToLast--) {
for (let index = 0; index <= nextToLast; index++) {
if (this.outOfOrder(index)) this.swap(index);
this.operations++;
}
}
return this.operations;
}
} La méthode doSort ne sait rien du tableau ni du type stocké : elle appelle outOfOrder pour divers indices et décide d'échanger ou non. On peut alors créer autant de dérivés qu'on veut de types à trier.
class IntBubbleSorter extends BubbleSorter {
private array: number[] = [];
sort(theArray: number[]): number {
this.array = theArray;
this.length = theArray.length;
return this.doSort();
}
protected swap(index: number): void {
const temp = this.array[index];
this.array[index] = this.array[index + 1];
this.array[index + 1] = temp;
}
protected outOfOrder(index: number): boolean {
return this.array[index] > this.array[index + 1];
}
} Voici l'une des formes classiques de réutilisation en orienté objet : l'algorithme générique vit dans la classe de base et se transmet par héritage à divers contextes détaillés.
BubbleSorter {abstract}
# outOfOrder()
# swap()
▲
┌───────┴────────┐
IntBubbleSorter DoubleBubbleSorter Mais cette technique a un prix. L'héritage est une relation très forte : les dérivés sont inextricablement liés à leur classe de base. Les méthodes outOfOrder et swap d'IntBubbleSorter seraient exactement ce dont d'autres algorithmes de tri auraient besoin — et pourtant, il n'y a aucun moyen de les réutiliser ailleurs. En héritant de BubbleSorter, on a condamné IntBubbleSorter à rester lié pour toujours au tri à bulles. C'est ici que Strategy offre une autre voie.
Strategy : le contexte délègue à un objet interchangeable
Strategy résout le problème d'inversion des dépendances tout autrement. Plutôt que de placer l'algorithme générique dans une classe de base abstraite, on le met dans une classe concrète (ApplicationRunner). Les méthodes abstraites que l'algorithme doit appeler sont définies dans une interface (Application), et le contexte délègue à cette interface.
interface Application {
init(): void;
idle(): void;
cleanup(): void;
done(): boolean;
}
// Le contexte : il contient l'algorithme générique et délègue
// les détails à l'objet stratégie qu'on lui injecte.
class ApplicationRunner {
constructor(private readonly app: Application) {}
run(): void {
this.app.init();
while (!this.app.done()) this.app.idle();
this.app.cleanup();
}
}
// La stratégie concrète : elle ne connaît rien d'ApplicationRunner.
class FtoCStrategy implements Application {
private isDone = false;
init(): void {
/* ouvre l'entrée standard */
}
idle(): void {
const ligne = lireLigne();
if (ligne === null || ligne.length === 0) this.isDone = true;
else {
const fahr = parseFloat(ligne);
console.log(`F=${fahr}, C=${(5.0 / 9.0) * (fahr - 32)}`);
}
}
cleanup(): void {
console.log("ftoc exit");
}
done(): boolean {
return this.isDone;
}
}
// On assemble contexte et stratégie au point d'entrée.
new ApplicationRunner(new FtoCStrategy()).run(); Cette structure a ses bénéfices et ses coûts par rapport à Template Method. Strategy implique plus de classes et plus d'indirection ; le pointeur de délégation coûte un peu plus en temps et en mémoire que l'héritage. En revanche, si l'on avait beaucoup d'applications à exécuter, on pourrait réutiliser une seule instance d'ApplicationRunner et lui injecter de multiples implémentations d'Application, réduisant le couplage entre l'algorithme générique et les détails qu'il pilote. Dans la plupart des cas, aucun de ces coûts ne pèse vraiment ; le plus gênant reste la classe supplémentaire qu'exige Strategy.
Le tri, encore : la supériorité de Strategy
C'est sur le tri que l'avantage décisif de Strategy apparaît. On déplace swap, outOfOrder, length et setArray dans une interface SortHandle, et BubbleSorter devient une classe concrète qui délègue à ce manche.
interface SortHandle {
swap(index: number): void;
outOfOrder(index: number): boolean;
length(): number;
setArray(array: unknown): void;
}
class BubbleSorter {
private operations = 0;
constructor(private readonly handle: SortHandle) {}
sort(array: unknown): number {
this.handle.setArray(array);
const length = this.handle.length();
this.operations = 0;
if (length <= 1) return this.operations;
for (let nextToLast = length - 2; nextToLast >= 0; nextToLast--) {
for (let index = 0; index <= nextToLast; index++) {
if (this.handle.outOfOrder(index)) this.handle.swap(index);
this.operations++;
}
}
return this.operations;
}
}
class IntSortHandle implements SortHandle {
private array: number[] = [];
setArray(array: unknown): void {
this.array = array as number[];
}
length(): number {
return this.array.length;
}
swap(index: number): void {
const temp = this.array[index];
this.array[index] = this.array[index + 1];
this.array[index + 1] = temp;
}
outOfOrder(index: number): boolean {
return this.array[index] > this.array[index + 1];
}
} Remarquez qu'IntSortHandle ne connaît rien de BubbleSorter : il n'a aucune dépendance envers l'implémentation du tri à bulles. Avec Template Method, ce n'était pas le cas — IntBubbleSorter dépendait directement de BubbleSorter, donc de l'algorithme. Template Method viole partiellement le DIP en liant swap et outOfOrder à l'algorithme du tri à bulles. Strategy n'a aucune dépendance de ce genre. On peut donc réutiliser IntSortHandle avec d'autres trieurs que BubbleSorter — par exemple un QuickBubbleSorter qui s'arrête tôt quand une passe trouve le tableau déjà ordonné :
class QuickBubbleSorter {
constructor(private readonly handle: SortHandle) {}
sort(array: unknown): number {
this.handle.setArray(array);
const length = this.handle.length();
let operations = 0;
if (length <= 1) return operations;
let passEnOrdre = false;
for (let nextToLast = length - 2; nextToLast >= 0 && !passEnOrdre; nextToLast--) {
passEnOrdre = true; // potentiellement
for (let index = 0; index <= nextToLast; index++) {
if (this.handle.outOfOrder(index)) {
this.handle.swap(index);
passEnOrdre = false;
}
operations++;
}
}
return operations;
}
} QuickBubbleSorter accepte lui aussi IntSortHandle, ou n'importe quel SortHandle. Voilà le bénéfice supplémentaire de Strategy : là où Template Method permet à un algorithme générique de manipuler plusieurs implémentations détaillées, Strategy — parce qu'il respecte pleinement le DIP — permet aussi à chaque implémentation détaillée d'être manipulée par plusieurs algorithmes génériques.
Piège courant
Héritage contre délégation : l'héritage de Template Method couple fortement le dérivé à la base, fige la structure et empêche de réutiliser les détails ailleurs. La délégation de Strategy est plus souple — détails et algorithme se réutilisent indépendamment — au prix d'une classe et d'une indirection de plus. Suivez le conseil du GoF : préférez la composition (donc Strategy) par défaut, et ne réservez Template Method qu'aux cas où sa simplicité l'emporte vraiment.
Les deux patrons séparent les algorithmes de haut niveau des détails de bas niveau, et permettent de réutiliser les premiers indépendamment des seconds. Au prix d'un peu de complexité, de mémoire et de temps d'exécution supplémentaires, Strategy permet en plus de réutiliser les détails indépendamment de l'algorithme de haut niveau.
À retenir
- Command réduit une requête à un objet doté d'une seule méthode
execute(): sa simplicité est trompeuse, sa polyvalence quasi illimitée (contrôle matériel, transactions, files, faire/défaire). - Le découplage temporel et physique de Command permet de saisir et valider une transaction maintenant pour l'exécuter plus tard, et d'isoler la logique métier de l'interface de saisie.
- Active Object bâtit un multitâche minimal sur Command : un moteur déroule une file de commandes qui se réinsèrent elles-mêmes ; les tâches RTC ne bloquent jamais et partagent une seule pile.
- Template Method et Strategy résolvent le même problème — séparer un algorithme générique de ses détails au service du DIP — mais l'un par héritage, l'autre par délégation.
- Template Method loge le squelette de l'algorithme dans une classe de base abstraite et diffère les étapes aux sous-classes : réutilisation classique, mais couplage fort et détails non réutilisables.
- Strategy délègue à un objet interchangeable injecté dans le contexte : plus de classes et d'indirection, mais respect total du DIP — l'algorithme et les détails se réutilisent chacun de leur côté.
- Méfiez-vous de l'abus de patron : qu'un patron soit applicable ne le rend pas souhaitable. Préférez la composition à l'héritage par défaut, et ne sortez l'outil lourd que lorsque le besoin le justifie.