1. Introduction
Imaginez que vous avez un objet qui effectue certaines actions — par exemple un bouton ou notre Worker. En même temps il y a plein d'autres objets qui doivent réagir à ces actions. Si on code en dur tous les "listeners" possibles dans la classe Worker, maintenir ce code devient un vrai cauchemar : tout changement dans la liste des abonnés nécessitera des modifications à l'intérieur du Worker.
Ceci viole le principe Ouvert-Fermé (OCP) et est considéré comme une mauvaise pratique architecturale.
Patron Observer : idée générale
Le patron "Observateur" (Observer) résout ce problème. Il permet à un objet éditeur d'avertir un nombre arbitraire d'objets intéressés (les listeners) des changements intervenus, sans rien savoir sur qui sont ces listeners ni ce qu'ils font. L'éditeur "lance l'info", et ceux qui veulent réagissent comme ils veulent.
Analogie : s'abonner à une newsletter. La rédaction ou le canal (éditeur) envoie un nouveau message, et tous les abonnés (observateurs) le reçoivent. La rédaction ne sait pas qui sont ces personnes, et elle n'en a pas besoin.
Fait intéressant : "Observer" est tellement populaire qu'il fait partie officiellement des patterns du "Gang of Four" (GoF).
Observer en C# : incarnation via events et delegates
En C# le patron "Observateur" est implémenté "out of the box" via les mécanismes des events et des delegates. Un event est un point d'extension sur lequel différents handlers peuvent s'abonner. Au lieu de maintenir manuellement une liste d'abonnés, c'est le mécanisme du langage qui s'en charge. Ci‑dessous on regarde d'abord une implémentation "manuelle", puis l'implémentation basée sur les events.
2. Implémentation classique d'Observer sans events
Voyons à quoi ça ressemblerait si le langage n'avait pas d'events :
// Interface de l'observateur
public interface IObserver
{
void Update(string message);
}
// Éditeur
public class Worker
{
private List<IObserver> observers = new List<IObserver>();
public void Subscribe(IObserver observer)
{
observers.Add(observer);
}
public void Unsubscribe(IObserver observer)
{
observers.Remove(observer);
}
public void DoWork()
{
Console.WriteLine("Worker travaille...");
NotifyObservers("Travail terminé !");
}
private void NotifyObservers(string message)
{
foreach (var observer in observers)
{
observer.Update(message);
}
}
}
// Observateur concret
public class WorkListener : IObserver
{
public void Update(string message)
{
Console.WriteLine($"
WorkListener a reçu le message : {message}");
}
}
Initialisation :
var worker = new Worker();
var listener = new WorkListener();
worker.Subscribe(listener);
worker.DoWork();
À noter : Ici la liste des abonnés (List<IObserver> observers) est gérée manuellement, et l'abonnement/désabonnement sont des méthodes explicites Subscribe/Unsubscribe.
3. Events et delegates — implémentation "haut niveau" d'Observer
On peut implémenter la même chose plus simplement et proprement avec des events. C'est l'Observer à la sauce C# :
public class Worker
{
public event EventHandler<WorkCompletedEventArgs>? WorkCompleted;
public void DoWork()
{
Console.WriteLine("Worker travaille...");
OnWorkCompleted("Travail terminé !");
}
protected virtual void OnWorkCompleted(string message)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs { Message = message });
}
}
public class WorkCompletedEventArgs : EventArgs
{
public string Message { get; set; }
}
public class WorkListener
{
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Console.WriteLine($"WorkListener a reçu le message : {e.Message}");
}
}
// Abonnement :
var worker = new Worker();
var listener = new WorkListener();
worker.WorkCompleted += listener.OnWorkCompleted;
worker.DoWork();
Avantages de cette approche :
- Pas besoin de maintenir manuellement la liste des abonnés.
- On bénéficie de toutes les capacités des events : abonnements multiples, désabonnement, lambdas.
- Sécurité garantie : seul l'éditeur peut déclencher l'event.
- Faible couplage : l'éditeur ne sait rien sur les listeners.
4. Comment "l'observateur" s'intègre à notre application
Intégrons le patron Observer dans notre application console. Que le Worker ait un nombre quelconque de handlers qui vont réagir à la fin du travail de façons différentes : certains affichent en console, d'autres comptent le nombre de travaux effectués, d'autres envoient un mail du style "Chef ! Tout est fait !".
Élargissons le code avec des exemples
// Second listener compteur
public class WorkCounter
{
public int Count { get; private set; }
public void OnWorkCompleted(object? sender, WorkCompletedEventArgs e)
{
Count++;
Console.WriteLine($"Travail pris en compte. Total : {Count} effectué(s).");
}
}
// Création des objets
var worker = new Worker();
var listener = new WorkListener();
var counter = new WorkCounter();
// Deux abonnements
worker.WorkCompleted += listener.OnWorkCompleted;
worker.WorkCompleted += counter.OnWorkCompleted;
// Simulation de plusieurs travaux
worker.DoWork();
worker.DoWork();
// Output:
// Worker travaille...
// WorkListener a reçu le message : Travail terminé !
// Travail pris en compte. Total : 1 effectué(s).
// Worker travaille...
// WorkListener a reçu le message : Travail terminé !
// Travail pris en compte. Total : 2 effectué(s).
Ainsi, vous ajoutez des "observateurs" au fur et à mesure, sans changer une seule ligne dans le code du Worker. La classe Worker reste inchangée, et le comportement du système est étendu via les abonnés.
5. Nuances utiles
Exemple réel : Observer dans les interfaces graphiques
Le patron Observer est la base de tous les frameworks GUI. Dans Windows Forms ou WPF, le clic sur un bouton déclenche l'event Click. Vous écrivez des handlers (observateurs) qui réagiront à cet event — ni votre classe Button, ni la bibliothèque .NET n'ont besoin de connaître vos abonnés.
// En WPF ou WinForms (à peu près)
myButton.Click += (s, e) => MessageBox.Show("L'utilisateur a cliqué sur le bouton !");
Observer dans des projets réels
- Interface utilisateur (réaction aux clics, changements, timers, etc.).
- Systèmes de notifications et d'événements.
- Plugins pour systèmes extensibles (le cœur génère des events, les extensions s'y abonnent).
- Systèmes distribués et moteurs de jeux (chaînes réactives faiblement couplées).
En bref, si vous avez besoin d'un système extensible où certaines parties peuvent réagir aux changements d'autres parties — Observer indispensable !
7. Particularités, erreurs typiques et prévention
Difficultés potentielles avec Observer
Fuites mémoire. Si un subscriber s'est abonné à un event mais ne s'est pas désabonné (surtout dans des objets longue durée), le garbage collector ne pourra pas libérer cet objet parce que l'éditeur garde encore une référence via le delegate de l'event. C'est critique si le subscriber n'est plus nécessaire mais l'éditeur continue d'exister.
Double abonnement. Si le même handler est abonné deux fois, il sera appelé deux fois — résultat : actions dupliquées et effets inattendus.
Exceptions dans les handlers. Si un des handlers lance une exception, l'exécution des abonnés suivants peut être interrompue. Pensez à rendre les handlers robustes et, si besoin, invoquez-les manuellement dans un try-catch pour que les autres abonnés puissent quand même s'exécuter en cas d'erreur d'un seul.
Schéma fréquent de fuites
flowchart LR
Publisher["Éditeur
(Worker)"] -- évènement --> ObserverA["Observateur A (Actif !)"]
Publisher -- évènement --> ObserverB["Observateur B (Fuite : oubli de désabonnement)"]
GO TO FULL VERSION