Aller au contenu

Observer

Design Pattern Observer

Propager les changements sans couplage fort

Introduction

Le design pattern Observer est un pattern de conception de comportement dont l'objectif est de définir une relation de dépendance entre plusieurs objets. Un objet notifie automatiquement tous les objets qui se sont abonnés lorsqu'un changement d'état ou un événement particulier s'est produit.

Ce design pattern favorise un faible couplage entre les composants : l'objet qui émet les notifications ne connaît pas les traitements réalisés par les objets abonnés. Il se contente de les contacter afin de leur demander d'exécuter un traitement.

Ce design pattern est particulièrement adapté pour implémenter des systèmes de notifications, des interfaces graphiques, des traitements événementiels ou encore des architectures orientées événements.

Problématique

Dans une application, il est fréquent qu'une modification d'un objet entraîne plusieurs traitements, qui sont exécutés par d'autres objets. Prenons l'exemple d'une boutique en ligne, qui doit réaliser plusieurs traitements lorsqu'elle valide une commande d'un client :

  • Envoyer un e-mail au client

  • Mettre à jour le stock

  • Générer une facture

Sans utiliser le design pattern Observer, la méthode Validate de la classe OrderService exécute directement chacun de ces traitements :

public class OrderService
{
    public void Validate(Order order)
    {
        var emailService = new EmailService();
        var stockService = new StockService();
        var invoiceService = new InvoiceService();

        emailService.Send(order);

        stockService.Update(order);

        invoiceService.GenerateInvoice(order);
    }
}
Cette implémentation présente plusieurs inconvénients :

  • La classe possède de nombreuses dépendances fortes vers les objets emailService, stockService, invoiceService.

  • L'ajout d'un traitement supplémentaire nécessite de modifier cette méthode.

  • Elle ne respecte pas le principe SOLID OCP (Open/Closed).

Fonctionnement

Le design pattern Observer permet à la méthode Validate d'exécuter les traitements à réaliser après la validation d'une commande, sans dépendance forte avec les objets exécutant ces traitements. Il met en œuvre deux principaux acteurs :

  • Le sujet représente l'objet observé qui connaît la liste des observateurs et les invoque pour exécuter les traitements nécessaires.

  • Les observateurs s'abonnent au sujet afin d'être automatiquement invoqués.

Le sujet ne connaît pas le fonctionnement des traitements exécutés par les observateurs. Ainsi, de nouveaux traitements peuvent être ajoutés sans modifier le sujet.

Conception

Voici un diagramme de classes présentant ces principaux acteurs :

  • La classe OrderService est le sujet.

  • Les classes EmailObserver, StockObserver et InvoiceObserver sont les observateurs :

Le sujet référence les observateurs au travers de l'interface IOrderObserver :

Design Pattern Observer : conception

Implémentation

Implémentation du sujet

Nous commençons par implémenter le contrat que devront respecter les observateurs, l'interface IOrderObserver. Elle expose une méthode OnOrderValidated qui sera invoquée par le sujet :

public interface IOrderObserver
{
    void OnOrderValidated(Order order);
}

La classe OrderService est le sujet :

public class OrderService

public class OrderService
{
    private readonly List<IOrderObserver> _observers = new();

    public void Subscribe(IOrderObserver observer)
    {
        _observers.Add(observer);
    }

    public void Unsubscribe(IOrderObserver observer)
    {
        _observers.Remove(observer);
    }

    public void Validate(Order order)
    {
        Console.WriteLine("Commande validée.");

        foreach (var observer in _observers)
            observer.OnOrderValidated(order);
    }
}

Implémentons des observateurs

Voici l'implémentation de trois observateurs, correspondant aux traitements présentés ci-dessus :

  • Voici un observateur permettant d'envoyer un email :
public class EmailObserver : IOrderObserver
{
    public void OnOrderValidated(Order order)
    {
        Console.WriteLine("Envoi de l'e-mail.");
    }
}
  • Voici un observateur permettant de mettre à jour le stock :
public class StockObserver : IOrderObserver
{
    public void OnOrderValidated(Order order)
    {
        Console.WriteLine("Mise à jour du stock.");
    }
}
  • Voici un observateur permettant de générer la facture :
public class InvoiceObserver : IOrderObserver
{
    public void OnOrderValidated(Order order)
    {
        Console.WriteLine("Génération de la facture.");
    }
}

Et voici le code permettant d'abonner les observateurs au sujet :

var orderService = new OrderService();

orderService.Subscribe(new EmailObserver());
orderService.Subscribe(new StockObserver());
orderService.Subscribe(new InvoiceObserver());

orderService.Validate(order);

L'exécution de la validation d'une commande affiche les messages suivants :

Commande validée.
Envoi de l'e-mail.
Mise à jour du stock.
Génération de la facture.

Maintenant, ajouter un nouveau traitement consiste simplement à créer un nouvel observateur et à l'enregistrer auprès du sujet. Aucune modification de la méthode Validate de la classe OrderService n'est nécessaire.

Intégration avec l'injection de dépendances

Les observateurs peuvent être obtenus grâce à l'injection de dépendances. Pour ce faire, nous enregistrons les observateurs dans le conteneur d'injection de dépendances :

builder.Services.AddScoped<IOrderObserver, EmailObserver>();
builder.Services.AddScoped<IOrderObserver, StockObserver>();
builder.Services.AddScoped<IOrderObserver, InvoiceObserver>();

Le sujet les obtient via une injection de dépendances :

public class OrderService
{
    private readonly IEnumerable<IOrderObserver> _observers;

    public OrderService(IEnumerable<IOrderObserver> observers)
    {
        _observers = observers;
    }

    public void Validate(Order order)
    {
        foreach (var observer in _observers)
            observer.OnOrderValidated(order);
    }
}

Maintenant, chaque nouvel observateur est automatiquement pris en compte dès son enregistrement dans le conteneur d'injection de dépendances. Dans cette variante, le conteneur d'injection de dépendances joue le rôle du mécanisme d'abonnement. Les observateurs sont découverts automatiquement au moment de la création du sujet, sans qu'il soit nécessaire d'appeler explicitement la méthode Subscribe.

Avantages et limites

Les avantages d'utilisation de ce design pattern sont les suivants :

  • Faible couplage entre le sujet et les observateurs.

  • Favorise le respect du principe SOLID OCP (Open/Closed).

  • Ajout de nouveaux traitements sans modifier le sujet.

  • Intégration naturelle avec l'injection de dépendances.

Et les limites auxquelles il faut prêter attention :

  • Le flux d'exécution peut devenir moins explicite lorsque les observateurs sont nombreux.

  • Une exception levée par un observateur peut empêcher les suivants d'être notifiés si elle n'est pas correctement gérée.

  • Lorsque plusieurs observateurs sont enregistrés, leur ordre d'exécution peut devenir important et nécessiter une gestion spécifique. Avec l'injection de dépendances, cet ordre dépend généralement de leur ordre d'enregistrement dans le conteneur.

Les événements dans les applications .NET

Le langage C# intègre nativement un mécanisme d'événements (event), qui met en œuvre les principes du design pattern Observer. Un objet peut exposer un événement auquel d'autres objets s'abonnent à l'aide des opérateurs += et -=. Lorsque l'événement est déclenché, tous les abonnés sont automatiquement notifiés.

Voici un exemple simplifié, dans lequel le sujet déclare l'événement OrderValidated :

public class OrderService
{
    public event Action<Order> OrderValidated;

    public void Validate(Order order)
    {
        // Validation de la commande ...

        OrderValidated?.Invoke(order);
    }
}

Un observateur peut ensuite s'abonner à cet évènement :

var orderService = new OrderService();

orderService.OrderValidated += order =>
{
    Console.WriteLine("Envoi de l'e-mail.");
};

orderService.Validate(order);

Cette approche repose sur le même principe que le design pattern Observer : le sujet ne connaît pas les traitements exécutés par les abonnés. Il se contente de déclencher un événement.

Cependant, les événements .NET présentent quelques différences avec une implémentation basée sur une interface :

  • Les abonnés sont des délégués plutôt que des objets implémentant une interface.

  • Ils s'intègrent moins naturellement avec l'injection de dépendances et rendent parfois les tests unitaires plus complexes.

Pour ces raisons, dans les architectures modernes basées sur l'injection de dépendances, il est fréquent de privilégier une implémentation du design pattern Observer à l'aide d'interfaces, comme celle présentée dans cet article. Les événements en C# ne constituent pas un design pattern différent : ils représentent une implémentation native du design pattern Observer. Le choix entre les deux implémentations dépend principalement du contexte de l'application et du niveau de découplage recherché.