Design Pattern Mediator
Centraliser les interactions entre plusieurs objets
Introduction
Le design pattern Mediator est un pattern de conception comportemental dont l'objectif est de centraliser les interactions entre plusieurs objets. Au lieu de communiquer directement entre eux, les différents objets passent par un objet intermédiaire, appelé médiator, chargé de coordonner leurs échanges. Il permet de réduire les couplages forts entre les classes, et facilite l'évolution, la maintenance et les tests.
Problématique
Nous développons une application commerciale permettant de traiter des commandes. Lorsqu'une commande est validée, plusieurs opérations doivent être réalisées :
- Vérifier et mettre à jour le stock.
- Effectuer le paiement.
- Générer la facture.
- Envoyer une notification au client.
Nous disposons pour cela de plusieurs services, chacun de ces services étant implémenté dans une classe distincte :
public interface IStockService
{
bool IsAvailable(int productId, int quantity);
void RemoveFromStock(int productId, int quantity);
}
public class StockService : IStockService
{
public bool IsAvailable(int productId, int quantity)
{
Console.WriteLine("Vérification du stock.");
return true;
}
public void RemoveFromStock(int productId, int quantity)
{
Console.WriteLine("Mise à jour du stock.");
}
}
public interface IPaymentService
{
bool Pay(decimal amount);
}
public class PaymentService : IPaymentService
{
public bool Pay(decimal amount)
{
Console.WriteLine($"Paiement de {amount} €.");
return true;
}
}
public interface IInvoiceService
{
void Generate();
}
public class InvoiceService : IInvoiceService
{
public void Generate()
{
Console.WriteLine("Génération de la facture.");
}
}
public interface INotificationService
{
void Send(string message);
}
public class NotificationService : INotificationService
{
public void Send(string message)
{
Console.WriteLine($"Notification : {message}");
}
}
Une première implémentation (non présente ci-dessus) pourrait consister à faire collaborer directement ces différentes classes. Par exemple, le service chargé du paiement pourrait déclencher la génération de la facture, et le service de gestion du stock pourrait appeler le service de paiement. Au fur et à mesure que de nouveaux traitements sont ajoutés, les classes risquent alors de multiplier les dépendances entre elles :
Chaque classe doit connaître les autres classes avec lesquelles elle doit collaborer. Une modification du processus métier peut alors nécessiter la modification de plusieurs classes.
Conception
Le point essentiel est que la connaissance des interactions est déplacée vers le médiator (interface IOrderMediator). Voici le diagramme de classes :
Dans le design pattern Mediator, plusieurs participants interviennent :
-
Le mediator qui définit les opérations permettant aux différentes classes de communiquer. Il s'agit de l'interface
IOrderMediator. -
Le Concrete Mediator qui implémente la logique permettant de coordonner les classes. Il s'agit de la classe
OrderMediator. -
Les Colleagues qui représentent les interfaces participant aux interactions. Il s'agit des interfaces des services
IStockService,IPaymentService,IInvoiceServiceetINotificationService. -
Les Concrete Colleagues qui représentent les implémentations concrètes de ces interfaces. Il s'agit des classes
StockService,PaymentService,InvoiceServiceetNotificationService.
Implémentation
Le design pattern Mediator consiste à introduire un composant intermédiaire, chargé de coordonner les interactions entre les différents participants. Une commande est créée à partir de la classe suivante :
public class Order
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Amount { get; set; }
public Order(int productId, int quantity, decimal amount)
{
ProductId = productId;
Quantity = quantity;
Amount = amount;
}
}
Voici l'interface du médiator :
Nous pouvons ensuite implémenter le médiator :
public class OrderMediator : IOrderMediator
{
private readonly IStockService _stockService;
private readonly IPaymentService _paymentService;
private readonly IInvoiceService _invoiceService;
private readonly INotificationService _notificationService;
public OrderMediator(
IStockService stockService,
IPaymentService paymentService,
IInvoiceService invoiceService,
INotificationService notificationService)
{
_stockService = stockService;
_paymentService = paymentService;
_invoiceService = invoiceService;
_notificationService = notificationService;
}
public void ProcessOrder(Order order)
{
if (!_stockService.IsAvailable(order.ProductId, order.Quantity))
{
_notificationService.Send("Le produit n'est pas disponible.");
return;
}
if (!_paymentService.Pay(order.Amount))
{
_notificationService.Send("Le paiement a échoué.");
return;
}
_stockService.RemoveFromStock(order.ProductId, order.Quantity);
_invoiceService.Generate();
_notificationService.Send("La commande a été validée.");
}
}
Le médiator connaît les différentes classes et détermine dans quel ordre elles doivent intervenir. Les services lui sont fournis via des injections de dépendance. Les services, quant à eux, restent indépendants les uns des autres :
La logique de coordination est désormais centralisée dans la classe OrderMediator.
Utilisation
Maintenant, le code client n'a plus besoin de connaître l'ensemble des interactions nécessaires pour le traitement d'une commande, grâce à l'utilisation du médiator :
var mediator = new OrderMediator(
new StockService(),
new PaymentService(),
new InvoiceService(),
new NotificationService());
var order = new Order(44, 3, 69.79m);
mediator.ProcessOrder(order);
Avantages
Le principal avantage du design pattern Mediator est la réduction du couplage entre les classes. Chaque service peut s'exécuter sans connaître les autres services impliqués dans le processus.
La logique de coordination est également regroupée dans un seul emplacement. Il devient ainsi plus simple de comprendre comment les différents services collaborent et de modifier leurs interactions.
Enfin, les différents services deviennent plus facilement réutilisables et testables, puisqu'ils contiennent principalement leur propre logique et ne dépendent pas de nombreux autres services.
Inconvénients
Le principal inconvénient du design pattern Mediator est que le médiator peut progressivement concentrer une quantité importante de logique. Lorsque le nombre d'interactions devient important, celui-ci peut devenir une classe volumineuse et complexe, parfois qualifiée de God Object.
Il est donc important de veiller à ce que le médiator conserve principalement la responsabilité de coordonner les interactions, et ne devienne pas le composant dans lequel toute la logique métier d'une fonctionnalité est regroupée.
Design patterns Mediator et Facade
Les design patterns Mediator et Facade introduisent tous les deux un objet intermédiaire, mais répondent à des problématiques différentes.
Une Facade fournit une interface simplifiée permettant à un code client d'utiliser un ensemble de classes d'un sous-système. Elle cherche principalement à simplifier l'utilisation d'un système complexe.
Un Mediator, quant à lui, organise les interactions entre plusieurs classes afin qu'elles n'aient pas besoin de communiquer directement entre elles.
On peut donc résumer leur différence ainsi : la Facade simplifie l'accès à un ensemble de services, tandis que le Mediator réduit les dépendances entre des objets qui doivent collaborer.
Design pattern Mediator et NuGet MediatR
Dans l'écosystème .NET, le nom Mediator est souvent associé au NuGet MediatR. Il est cependant important de distinguer le design pattern de la bibliothèque.
Le design pattern Mediator décrit un principe de conception. MediatR permet notamment d'implémenter des communications découplées à travers des requêtes, des commandes, des notifications et leurs gestionnaires.
L'utilisation de MediatR peut donc s'inscrire dans une architecture reposant sur des principes proches du pattern Mediator, mais utiliser le design pattern Mediator ne nécessite pas d'utiliser MediatR.


