Command
Design Pattern Command
Encapsuler une action dans un objet
Introduction
Le design pattern Command est un pattern de conception de comportement dont l’objectif est d’encapsuler une action dans un objet. Autrement dit, au lieu d'appeler directement une méthode sur un objet, le code encapsule la demande d'exécution de cette méthode dans un objet représentant cette action. Cette commande contient les informations nécessaires à son exécution et peut être manipulée indépendamment de son exécution.
Ce design pattern permet ainsi de dissocier l’objet qui déclenche une action de celui qui la réalise. Il est particulièrement adapté lorsque les actions doivent être journalisées, mises en attente, rejouées ou annulées.
Problématique
Prenons l’exemple d’une application permettant de valider ou d’annuler une commande d'un client. Sans utiliser le design pattern Command, la classe traitant les commandes utilise directement les services métiers :
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
}
public void Validate(Order order)
{
_orderService.Validate(order);
}
public void Cancel(Order order)
{
_orderService.Cancel(order);
}
}
Le contrôleur déclenche directement le traitement métier. Cette approche est simple, mais elle ne permet pas facilement de différer, journaliser, mettre en file d'attente ou rejouer les actions exécutées.
Lorsque les actions deviennent nombreuses, ou qu’elles doivent être stockées et exécutées ultérieurement, ce couplage peut devenir contraignant.
Fonctionnement
Le design pattern Command représente chaque action par une classe dédiée appelée commande, et toutes les commandes implémentent une interface commune. Par exemple :
Les principaux acteurs de ce design pattern sont :
-
La Command : définit le contrat commun.
-
La ConcreteCommand : encapsule une action concrète et les données nécessaires à son exécution.
-
Le Receiver : exécute le traitement associé à la commande.
-
L'Invoker : déclenche l'exécution d'une commande sans connaître son implémentation.
-
Le Client : crée la commande.
Conception
Dans ce diagramme de classes :
-
L'interface ICommand représente la Command.
-
Les classes ValidateOrderCommand et CancelOrderCommand représentent les ConcreteCommand.
-
L'interface IOrderService représente le Receiver et la classe OrderService représente le ConcreteReceiver.
-
La classe CommandExecutor représente l'Invoker.
Implémentation en C
Le service métier joue le rôle de Receiver. Voici un exemple dans lequel nous souhaitons valider ou annuler une commande :
public enum OrderStatus
{
Pending = 1,
Validated = 2,
Cancelled = 3
}
public interface IOrderService
{
void Validate(Order order);
void Cancel(Order order);
}
public class OrderService : IOrderService
{
public void Validate(Order order)
{
order.Status = OrderStatus.Validated;
Console.WriteLine($"La commande {order.Id} a été validée.");
}
public void Cancel(Order order)
{
order.Status = OrderStatus.Cancelled;
Console.WriteLine($"La commande {order.Id} a été annulée.");
}
}
Voici l'implémentation de la classe de base des commandes :
public abstract class OrderCommandBase : ICommand
{
protected readonly IOrderService _orderService;
protected readonly Order _order;
protected OrderCommandBase(IOrderService orderService, Order order)
{
_orderService = orderService;
_order = order;
}
public abstract void Execute();
}
La commande de validation (ConcreteCommand) encapsule le service ainsi que la commande métier concernée.
public class ValidateOrderCommand : OrderCommandBase
{
public ValidateOrderCommand(IOrderService orderService, Order order)
: base(orderService, order)
{
}
public override void Execute()
{
_orderService.Validate(_order);
}
}
La commande d’annulation (ConcreteCommand) suit le même principe :
public class CancelOrderCommand : OrderCommandBase
{
public CancelOrderCommand(IOrderService orderService, Order order)
: base(orderService, order)
{
}
public override void Execute()
{
_orderService.Cancel(_order);
}
}
L’Invoker
L’Invoker exécute les commandes sans connaître leur traitement concret :
Le CommandExecutor manipule toutes les commandes de manière uniforme, via l'interface ICommand. Il ne sait pas si l’action consiste à valider une commande, à l’annuler ou à réaliser un autre traitement ; tout dépend de la commande passée en paramètre de la méthode Execute :
IOrderService orderService = new OrderService();
var order = new Order(123, OrderStatus.Pending);
var executor = new CommandExecutor();
ICommand validateCommand = new ValidateOrderCommand(orderService, order);
executor.Execute(validateCommand);
ICommand cancelCommand = new CancelOrderCommand(orderService, order);
executor.Execute(cancelCommand);
Exécuter les commandes de manière différée
Puisqu’une action est encapsulée par un objet, elle peut être stockée dans une file d’attente :
public class CommandQueue
{
private readonly Queue<ICommand> _commands;
public CommandQueue()
{
_commands = [];
}
public void Add(ICommand command)
{
_commands.Enqueue(command);
}
public void ExecuteAll()
{
while (_commands.TryDequeue(out var command))
{
command.Execute();
}
}
}
Ajout des commandes dans la queue :
var queue = new CommandQueue();
queue.Add(validateCommand);
queue.Add(cancelCommand);
queue.ExecuteAll();
Ce mécanisme peut être utilisé pour les traitements différés, les tâches en arrière-plan, les systèmes de planification ou les systèmes de messagerie (RabbitMQ, Azure Service Bus, etc.).
Avantages
Le design pattern Command permet notamment de :
-
Réduire le couplage entre l’appelant et le traitement.
-
Mettre les actions en file d’attente.
-
Historiser ou rejouer les traitements.
-
Faciliter la journalisation, la gestion des erreurs et le suivi des traitements.
Cette approche favorise notamment les principes SOLID suivants :
-
OCP (Open/Closed Principle), puisque de nouvelles commandes peuvent être ajoutées sans modifier l'Invoker.
-
SRP (Single Responsibility Principle), puisque chaque commande encapsule une action.
Cependant, utilisé à mauvais escient ce design pattern peut introduire une complexité inutile pour des opérations simples.
