Bridge
Design Pattern Bridge
Faire évoluer une abstraction indépendamment de son implémentation
Introduction
Le design pattern Bridge est un pattern de conception structurel dont l'objectif est de séparer une abstraction de son implémentation, afin de permettre à ces deux éléments d'évoluer indépendamment.
Ce design pattern permet de résoudre un problème fréquent avec l'héritage : la multiplication des classes lorsque plusieurs caractéristiques d'un objet peuvent varier indépendamment. En séparant l'abstraction de son implémentation et en les reliant par composition, nous obtenons deux hiérarchies pouvant évoluer séparément.
Problématique
Notre application doit permettre d'envoyer des notifications standards et urgentes, qui peuvent être envoyées via différents canaux de communication, par e-mail et par SMS. Une première approche pourrait consister à créer une classe pour chaque combinaison :
Cette conception est correcte mais elle mélange deux dimensions différentes : le type et le mode de communication de la notification. Et ce problème s'accentue lorsque la conception évolue. Par exemple, si nous ajoutons un critère de temporisation, avec des notifications planifiées et des notifications publiées immédiatement, nous devons implémenter de nouvelles classes :
Et ainsi de suite. Chaque nouvelle variante d'une dimension doit être combinée avec les variantes de l'autre dimension. Le nombre de classes peut donc augmenter rapidement.
Solution proposée par le pattern Bridge
Le design pattern Bridge propose de séparer chacune de ces deux dimensions, de manière très distincte. L'objectif est de permettre aux types de notifications et aux moyens d'envoi d'évoluer indépendamment. Au travers de cet exemple, le design pattern Bridge utilise un principe important de la conception orientée objet : privilégier la composition lorsque l'héritage conduit à combiner plusieurs dimensions indépendantes.
Dans le diagramme de classes ci-dessous, l'arbre d'héritage à gauche représente l'abstraction, caractérisée par le type de notification. L'arbre d'héritage à droite représente les différentes implémentations, caractérisées par les modes de communication des notifications :
Au lieu qu'une notification hérite d'une classe correspondant à son mode d'envoi, elle possède une référence vers une abstraction représentant ce mode d'envoi. Nous obtenons donc deux arbres d'héritage indépendants et reliés par un pont, d'où le nom du design pattern Bridge.
La nature de la notification et son moyen d'envoi étant indépendants, nous pouvons implémenter différentes combinaisons, sans avoir besoin de créer une classe pour chacune d'entre elles.
Implémentation
Commençons par implémenter la classe permettant d'envoyer une notification :
public interface INotificationSender
{
void Send(string message);
}
public abstract class NotificationSender : INotificationSender
{
public abstract void Send(string message);
}
Puis les deux classes qui implémentent les deux modes d'envoi des notifications :
public class EmailNotificationSender : NotificationSender
{
public override void Send(string message)
{
Console.WriteLine($"Envoi par e-mail : {message}");
}
}
public class SmsNotificationSender : NotificationSender
{
public override void Send(string message)
{
Console.WriteLine($"Envoi par SMS : {message}");
}
}
Définissons maintenant l'abstraction Notification :
public abstract class Notification
{
protected INotificationSender Sender { get; }
protected Notification(INotificationSender sender)
{
Sender = sender;
}
public abstract void Send(string message);
}
La classe ne connaît pas l'implémentation concrète utilisée pour envoyer le message. Elle dépend uniquement de l'abstraction INotificationSender. Nous pouvons ensuite créer différentes variantes de notifications.
public class StandardNotification : Notification
{
public StandardNotification(INotificationSender sender)
: base(sender)
{
}
public override void Send(string message)
{
Sender.Send(message);
}
}
Une notification urgente peut enrichir le comportement avant de déléguer l'envoi :
public class UrgentNotification : Notification
{
public UrgentNotification(INotificationSender sender)
: base(sender)
{
}
public override void Send(string message)
{
Sender.Send($"URGENT : {message}");
}
}
Utilisation
Le code client peut maintenant combiner librement les deux dimensions :
INotificationSender emailSender = new EmailNotificationSender();
INotificationSender smsSender = new SmsNotificationSender();
// Notification standard par mail
Notification notification1 = new StandardNotification(emailSender);
notification1.Send("Votre mot de passe expire dans 1 mois.");
// Notification urgente par SMS
Notification notification2 = new UrgentNotification(smsSender);
notification2.Send("Votre mot de passe expire demain.");
Ajout d'une nouvelle implémentation
L'un des intérêts du pattern Bridge apparaît lorsque nous souhaitons ajouter un nouveau moyen d'envoi. Supposons que l'application doive maintenant gérer les notifications instantanées (Push) :
public class PushNotificationSender : NotificationSender
{
public void Send(string message)
{
Console.WriteLine($"PUSH : {message}");
}
}
Cette nouvelle implémentation peut immédiatement être utilisée avec les différents types de notifications existants :
var sender = new PushNotificationSender();
Notification standard = new StandardNotification(sender);
Notification urgent = new UrgentNotification(sender);
Nous n'avons pas besoin de créer des classes telles que StandardPushNotification et UrgentPushNotification. De la même manière, nous pourrions ajouter un nouveau type de notification sans modifier les différents moyens d'envoi. Ces deux notions concernant les notifications peuvent évoluer indépendamment.
Avantages
Le pattern Bridge apporte plusieurs avantages :
-
Séparation des responsabilités qui peuvent évoluer indépendamment.
-
Favorisation de la composition plutôt que la création de hiérarchies d'héritage complexes.
-
Ajout de nouvelles abstractions sans modifier les implémentations existantes.
-
Ajout de nouvelles implémentations sans modifier les abstractions existantes.
-
Tests unitaires facilités grâce à l'utilisation d'une abstraction pour les implémentations.
Cette conception respecte également le principe OCP (Open/Closed Principle) : la solution peut être étendue avec de nouveaux types de notifications ou de nouveaux moyens d'envoi sans nécessiter la modification des classes existantes.
Design pattern Bridge ou design pattern Adapter ?
Les design patterns Bridge et Adapter sont deux modèles de conception structurels qui semblent proches, car ils s'appuient tous les deux sur la composition et sur des abstractions. Cependant, leur intention est cependant différente.
Le design pattern Adapter permet de rendre compatible une classe existante avec une interface qu'elle ne respecte pas initialement. Il est généralement utilisé pour faire collaborer des éléments dont les interfaces sont incompatibles.
Le design pattern Bridge résulte d'un choix de conception. Il sépare volontairement deux dimensions d'un système afin qu'elles puissent évoluer indépendamment.
Ainsi, le design pattern Adapter rend compatibles deux éléments qui ne l'étaient pas. Le design pattern Bridge sépare les deux dimensions afin qu'elles puissent évoluer indépendamment.


