Aller au contenu principal
Design Patterns

Le Pattern Command

6 min de lecture
  • design-patterns
  • architecture
  • programmation
Command : l'action devenue objet

Un bouton dans une interface graphique déclenche une action. Un raccourci clavier déclenche la même action. Un script automatisé aussi. Si l'action est codée en dur dans chaque déclencheur, vous avez trois endroits à maintenir. Le pattern Command résout ce problème en encapsulant l'action dans un objet — un objet qu'on peut stocker, transmettre, annuler, rejouer.

Le principe

Le Command transforme une action en objet autonome. Cet objet contient toute l'information nécessaire pour exécuter l'action : la méthode à appeler, les paramètres, et éventuellement l'état pour annuler.

Les quatre acteurs :

  • Command — l'interface commune avec une méthode execute()
  • ConcreteCommand — l'implémentation qui encapsule une action précise
  • Invoker — celui qui déclenche la commande (bouton, raccourci, scheduler)
  • Receiver — celui qui exécute réellement le travail
Invoker → Command.execute() → Receiver.action()

L'Invoker ne sait pas ce que fait la commande. La commande ne sait pas qui l'a déclenchée. Le découplage est total.

Implémentation

// L'interface commune
interface Command {
  execute(): void;
  undo(): void;
}
 
// Le Receiver — celui qui fait le vrai travail
class TextEditor {
  private content: string = "";
 
  insert(text: string, position: number): void {
    this.content = this.content.slice(0, position) + text + this.content.slice(position);
  }
 
  delete(position: number, length: number): string {
    const deleted = this.content.slice(position, position + length);
    this.content = this.content.slice(0, position) + this.content.slice(position + length);
    return deleted;
  }
 
  getContent(): string {
    return this.content;
  }
}
 
// Les commandes concrètes
class InsertCommand implements Command {
  constructor(
    private editor: TextEditor,
    private text: string,
    private position: number
  ) {}
 
  execute(): void {
    this.editor.insert(this.text, this.position);
  }
 
  undo(): void {
    this.editor.delete(this.position, this.text.length);
  }
}
 
class DeleteCommand implements Command {
  private deletedText: string = "";
 
  constructor(
    private editor: TextEditor,
    private position: number,
    private length: number
  ) {}
 
  execute(): void {
    this.deletedText = this.editor.delete(this.position, this.length);
  }
 
  undo(): void {
    this.editor.insert(this.deletedText, this.position);
  }
}

L'Invoker avec historique

class CommandHistory {
  private history: Command[] = [];
  private undone: Command[] = [];
 
  execute(command: Command): void {
    command.execute();
    this.history.push(command);
    this.undone = []; // On efface le redo après une nouvelle action
  }
 
  undo(): void {
    const command = this.history.pop();
    if (command) {
      command.undo();
      this.undone.push(command);
    }
  }
 
  redo(): void {
    const command = this.undone.pop();
    if (command) {
      command.execute();
      this.history.push(command);
    }
  }
}

Utilisation

const editor = new TextEditor();
const history = new CommandHistory();
 
history.execute(new InsertCommand(editor, "Bonjour", 0));
// "Bonjour"
 
history.execute(new InsertCommand(editor, " le monde", 7));
// "Bonjour le monde"
 
history.undo();
// "Bonjour"
 
history.redo();
// "Bonjour le monde"

Les cas d'usage concrets

Undo / Redo

Le cas le plus classique. Chaque action utilisateur est une commande stockée dans un historique. Ctrl+Z dépile et appelle undo(). Ctrl+Y appelle redo(). C'est le modèle utilisé par les éditeurs de texte, les logiciels de dessin, et les IDE.

Files de tâches (Job Queues)

Les commandes sont des objets — elles peuvent être sérialisées et mises dans une file d'attente pour exécution différée :

// Créer la commande maintenant
const command = new SendEmailCommand(user, template);
 
// L'exécuter plus tard via une queue
queue.enqueue(command);
 
// Un worker la dépile et l'exécute
const cmd = queue.dequeue();
cmd.execute();

C'est le principe derrière BullMQ, Sidekiq, Celery — les systèmes de job queues.

Transactions et rollback

En encapsulant chaque opération dans une commande avec execute() et undo(), vous pouvez implémenter des transactions manuelles :

class Transaction {
  private commands: Command[] = [];
 
  add(command: Command): void {
    command.execute();
    this.commands.push(command);
  }
 
  rollback(): void {
    // Annuler dans l'ordre inverse
    while (this.commands.length > 0) {
      this.commands.pop()!.undo();
    }
  }
}

Macros

Une macro est simplement une liste de commandes exécutées en séquence :

class MacroCommand implements Command {
  constructor(private commands: Command[]) {}
 
  execute(): void {
    this.commands.forEach(cmd => cmd.execute());
  }
 
  undo(): void {
    [...this.commands].reverse().forEach(cmd => cmd.undo());
  }
}

Command vs Strategy vs Observer

Ces trois patterns sont souvent confondus :

PatternEncapsuleObjectif
CommandUne actionDécoupler émetteur et exécuteur, permettre undo/redo
StrategyUn algorithmeChoisir un comportement à l'exécution
ObserverUne notificationRéagir à un événement

La différence clé : le Command stocke l'action pour l'exécuter, l'annuler ou la rejouer plus tard. Le Strategy remplace un comportement. L'Observer notifie.

Les limites

Explosion de classes. Chaque action nécessite sa propre classe Command. Pour une application avec 50 actions, ça fait 50 classes. C'est le prix du découplage.

Complexité du undo. Certaines actions sont difficiles à annuler — supprimer un fichier, envoyer un email, appeler une API externe. Le undo n'est pas toujours possible et il faut le gérer explicitement.


Le pattern Command transforme les actions en objets de première classe. Cette transformation ouvre des possibilités que le code procédural ne permet pas : historique, annulation, exécution différée, composition. C'est le pattern derrière chaque Ctrl+Z que vous utilisez au quotidien.