Accueil / Tutoriels / Command : encapsuler des actions pour mieux les contrôler

Command : encapsuler des actions pour mieux les contrôler

Publié le 16 Juin 2025
Conception

Command (Commande en français) est un design pattern qui permet d'encapsuler une requête sous forme d'objet, rendant possible sa mise en file, son annulation, sa journalisation ou encore sa réexécution. Ce pattern trouve des applications concrètes dans de nombreuses situations en programmation web, en particulier dans les systèmes complexes nécessitant une séparation claire entre l'émetteur d'une action et son exécution.

Principe du pattern Command

Le pattern Command repose sur une idée simple : découpler l'émetteur d'une action de son exécution réelle. Plutôt que de faire appel directement à une méthode ou une fonction, on encapsule cette opération dans un objet "commande" qui expose une interface standard. Ce dernier peut alors être transmis, stocké, mis en file d’attente ou rejoué plus tard.

Ce découplage apporte une plus grande modularité du code, facilite l’ajout de nouvelles commandes sans impacter l’émetteur et permet des comportements avancés comme l’annulation ou la journalisation. Dans un environnement web, cela permet notamment de mieux structurer le code métier, d’automatiser des séquences d’actions, ou encore d’implémenter des workflows utilisateur complexes.

Exemples d’utilisations du pattern Command

Centraliser les actions utilisateurs dans une interface administrateur

Dans une interface d’administration d’un CMS ou d’une application métier, chaque action réalisée par un utilisateur (créer un article, activer un utilisateur, supprimer une image, etc…) peut être encapsulée dans une commande. On peut ainsi les journaliser, les valider ou les rejouer (utile dans un système de "draft" ou d’annulation). Ce pattern permet aussi de construire un moteur de règles qui traite les commandes selon des droits ou des conditions dynamiques.

Implémenter une file d'attente pour des traitements asynchrones

Dans un système e-commerce, le traitement d’une commande client peut impliquer plusieurs étapes (validation, génération de facture, envoi d’e-mail, mise à jour du stock). Chacune de ces étapes peut être encapsulée dans une commande. Cela permet de les stocker dans une file d’attente (ex. via un message ou une base de données), de les exécuter de manière asynchrone et de rejouer certaines opérations en cas d’échec.

Points d’attention

Le principal écueil du pattern Command réside dans la prolifération de classes. Chaque commande devient une entité à part entière, ce qui alourdit la base de code si le nombre d’actions est important. On peut vite perdre en lisibilité.

Un autre point d’attention est la gestion de l’état interne des commandes. Si une commande dépend fortement du contexte au moment de son exécution, il faut s’assurer que ce contexte est capturé de manière fiable lors de la création de l’objet.

Enfin, mal utilisé, le pattern peut introduire une complexité inutile dans des cas simples où un appel direct aurait suffi. Il faut donc bien évaluer le rapport coût/bénéfice avant de l’adopter.

Implémenter le pattern Command

Nous allons définir une interface ou une abstraction commune que chaque commande concrète doit respecter. Cette interface expose généralement une méthode standard, souvent nommée execute(), qui représente l’action à accomplir.

Chaque commande concrète encapsule une action précise, avec ses éventuels paramètres. Elle sait quoi faire, mais ne s’exécute pas seule. Elle délègue cette exécution à un objet appelé Receiver, qui contient la logique métier réelle. C’est lui qui connaît les détails concrets de l’opération à effectuer.

Entre ces deux éléments, on introduit un troisième acteur : l’Invoker. C’est lui qui orchestre l’appel des commandes, sans jamais connaître leur nature exacte ni leur implémentation. Il peut se contenter d’appeler execute() sur n’importe quelle commande, ce qui le rend extrêmement flexible. Il peut enchaîner plusieurs commandes, les stocker dans une file d’attente, les exécuter de façon conditionnelle ou encore enregistrer un historique pour permettre leur rejouabilité.

Dans la pratique, le Receiver est le plus souvent injecté dans la commande au moment de sa création. Cela permet de créer des commandes autonomes, prêtes à être exécutées par n’importe quel Invoker, dans n’importe quel contexte. C’est cette séparation des rôles qui fait la force du pattern Command : chaque acteur se concentre sur une seule responsabilité, ce qui rend le système extensible, testable et évolutif.

Voici un exemple en Go :

Prenons l’exemple d’une application qui gère des actions utilisateur comme l’envoi d’un e-mail ou la génération d’un rapport. Ces actions sont encapsulées sous forme de commandes et mises en file pour être exécutées de manière asynchrone.


package command

// Command définit l'interface que toutes les commandes concrètes doivent implémenter.
type Command interface {
   Execute()
}


package command

// Queue CommandQueue gère une file de commandes à exécuter.
type Queue struct {
   queue []Command
}

// NewQueue crée et renvoie une nouvelle file de commandes vide.
func NewQueue() *Queue {
   return &Queue{
      queue: []Command{},
   }
}

// AddCommand ajoute une commande dans la file.
func (cq *Queue) AddCommand(cmd Command) {
   cq.queue = append(cq.queue, cmd)
}

// Process exécute toutes les commandes en attente et vide ensuite la file.
func (cq *Queue) Process() {
   for _, cmd := range cq.queue {
      cmd.Execute()
   }
   cq.queue = []Command{}
}


package command

import "training.go/designpatterns/command/service"

// SendWelcomeEmailCommand encapsule l'action d'envoi d'un e-mail de bienvenue.
type SendWelcomeEmailCommand struct {
   emailService *service.EmailService
   userEmail    string
}

// NewSendWelcomeEmailCommand construit la commande avec son Receiver et l'adresse e-mail du destinataire.
func NewSendWelcomeEmailCommand(emailService *service.EmailService, userEmail string) *SendWelcomeEmailCommand {
   return &SendWelcomeEmailCommand{
      emailService: emailService,
      userEmail:    userEmail,
   }
}

// Execute délègue au Receiver (EmailService) l'envoi de l'e-mail.
func (c *SendWelcomeEmailCommand) Execute() {
   c.emailService.SendWelcomeEmail(c.userEmail)
}


package service

import "fmt"

// EmailService est le Receiver qui connaît la logique métier d'envoi d'e-mail.
type EmailService struct{}

// NewEmailService crée une instance d'EmailService.
func NewEmailService() *EmailService {
   return &EmailService{}
}

// SendWelcomeEmail contient la logique concrète pour envoyer l'e-mail de bienvenue.
func (e *EmailService) SendWelcomeEmail(userEmail string) {
   fmt.Printf("Welcome email sent to %s\n", userEmail)
}


package main

import (
   "designpatterns/command/command"
   "designpatterns/command/service"
)

func main() {
   emailSvc := service.NewEmailService()

   // Création de la commande en lui passant le Receiver et l'adresse e-mail cible.
   cmd := command.NewSendWelcomeEmailCommand(emailSvc, "toto@example.com")

   // Instanciation de l'Invoker (la file de commandes), ajout et exécution.
   queue := command.NewQueue()
   queue.AddCommand(cmd)
   queue.Process()
}

Le même exemple en PHP :


namespace Practice\DesignPatterns\Command;

interface CommandInterface
{
   public function execute(): void;
}


namespace Practice\DesignPatterns\Command;

class CommandQueue {
   private array $queue = [];

   public function addCommand(CommandInterface $command): void
   {
       $this->queue[] = $command;
   }

   public function process(): void
   {
       foreach ($this->queue as $command) {
           $command->execute();
       }
       $this->queue = [];
   }
}


namespace Practice\DesignPatterns\Command;

class EmailService {
   public function sendWelcomeEmail(string $user): void
   {
       echo "Email de bienvenue envoyé à $user\n";
   }
}


namespace Practice\DesignPatterns\Command;

class SendWelcomeEmailCommand implements CommandInterface {
   private EmailService $emailService;
   private string $user;

   public function __construct(EmailService $emailService, string $user)
   {
       $this->emailService = $emailService;
       $this->user = $user;
   }

   public function execute(): void
   {
       $this->emailService->sendWelcomeEmail($this->user);
   }
}


declare(strict_types=1);

use Practice\DesignPatterns\Command\CommandQueue;
use Practice\DesignPatterns\Command\EmailService;
use Practice\DesignPatterns\Command\SendWelcomeEmailCommand;

require "./vendor/autoload.php";

$emailService = new EmailService();
$command = new SendWelcomeEmailCommand($emailService, "toto@example.com");

$queue = new CommandQueue();
$queue->addCommand($command);
$queue->process();

Conclusion

Le pattern Command s’impose comme un excellent allié dans la structuration de systèmes où les actions doivent être contrôlées, différées ou historisées. Sa capacité à encapsuler les comportements permet d’introduire des mécanismes puissants de rejouabilité, de validation ou de permission. Utilisé avec parcimonie et rigueur, il contribue à rendre les applications web plus modulaires, testables et évolutives. C’est un outil à maîtriser pour tout développeur qui cherche à aller au-delà du simple enchaînement de fonctions.

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern Command. Vous pouvez retrouver mon article sur les patrons de conception ou les autres tutoriels sur Proxy, Builder, Decorator, Chain of responsibility et Factory.