Dans cet article, vous découvrirez le modèle de conception Observer dans .NET C# avec quelques améliorations.
Le modèle de conception d'observateur est l'un des modèles de conception les plus importants et les plus couramment utilisés.
Tout d'abord, vérifions la définition formelle de l' Observer Design Pattern .
Selon
Le modèle de conception d'observateur permet à un abonné de s'inscrire et de recevoir des notifications d'un fournisseur. Il convient à tout scénario nécessitant une notification push. Le modèle définit un fournisseur (également appelé sujet ou observable) et zéro, un ou plusieurs observateurs. Les observateurs s'enregistrent auprès du fournisseur et chaque fois qu'une condition, un événement ou un changement d'état prédéfini se produit, le fournisseur notifie automatiquement tous les observateurs en appelant l'une de leurs méthodes. Dans cet appel de méthode, le fournisseur peut également fournir des informations sur l'état actuel aux observateurs. Dans .NET, le modèle de conception Observer est appliqué en implémentant les interfaces génériques System.IObservable<T> et System.IObserver<T> . Le paramètre de type générique représente le type qui fournit les informations de notification.
Comme nous le savons maintenant, l' Observer Design Pattern formule la relation entre les modules Observable et Observer . Ce qui rend le modèle de conception d'observateur unique, c'est qu'en l'utilisant, vous pouvez y parvenir sans avoir une relation étroitement couplée.
En analysant le fonctionnement du modèle, vous trouverez ce qui suit :
Ce sont les abstractions utilisées pour implémenter le modèle de conception Observer dans .NET C# .
Il s'agit d'une interface Covariant représentant n'importe quel Observable . Si vous voulez en savoir plus sur la variance dans .NET, vous pouvez consulter l'article
Les membres définis dans cette interface sont :
public IDisposable Subscribe (IObserver<out T> observer);
La méthode Subscribe
doit être appelée pour informer l' Observable qu'un certain Observer est intéressé par son flux d'informations.
La méthode Subscribe
renvoie un objet qui implémente l'interface IDisposable
. Cet objet pourrait alors être utilisé par l' Observer pour se désabonner du flux d'informations fourni par l' Observable . Une fois cela fait, l' observateur ne serait pas informé des mises à jour du flux d'informations.
Il s'agit d'une interface Contravariant représentant n'importe quel Observer . Si vous voulez en savoir plus sur la variance dans .NET, vous pouvez consulter l'article
Les membres définis dans cette interface sont :
public void OnCompleted (); public void OnError (Exception error); public void OnNext (T value);
La méthode OnCompleted
doit être appelée par l' Observable pour informer l' Observer que le flux d'informations est terminé et que l' Observer ne doit plus attendre d'informations.
La méthode OnError
doit être appelée par l' Observable pour informer l' Observer qu'une erreur s'est produite.
La méthode OnNext
doit être appelée par l' Observable pour informer l' Observer qu'une nouvelle information est prête et ajoutée au flux.
Voyons maintenant comment Microsoft recommande d'implémenter le modèle de conception Observer en C#. Plus tard, je vous montrerai quelques améliorations mineures que j'ai implémentées moi-même.
Nous allons créer une simple application de console de prévisions météorologiques . Dans cette application, nous aurons le module WeatherForecast (Observable, Provider, Subject) et le module WeatherForecastObserver (Observer).
Alors, commençons à regarder dans la mise en œuvre.
namespace Observable { public class WeatherInfo { internal WeatherInfo(double temperature) { Temperature = temperature; } public double Temperature { get; } } }
Il s'agit de l'entité représentant l'élément d'information devant circuler dans le flux d'informations.
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecast : IObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IDisposable Subscribe(IObserver<WeatherInfo> observer) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } return new WeatherForecastUnsubscriber(m_Observers, observer); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
Ce que l'on peut remarquer ici :
WeatherForecast
implémente IObservable<WeatherInfo>
.Subscribe
, nous vérifions si le passé dans Observer a déjà été enregistré auparavant ou non. Sinon, nous l'ajoutons à la liste locale des observateurs m_Observers
. Ensuite, nous bouclons sur toutes les entrées WeatherInfo
que nous avons dans la liste locale m_WeatherInfoList
une par une et en informons l'Observer en appelant la méthode OnNext
de l'Observer.WeatherForecastUnsubscriber
à utiliser par l'observateur pour se désabonner du flux d'informations.RegisterWeatherInfo
est définie pour que le module principal puisse enregistrer de nouvelles WeatherInfo
. Dans le monde réel, cela pourrait être remplacé par un appel d'API interne programmé ou un écouteur vers un hub SignalR ou quelque chose d'autre qui agirait comme une source d'informations.
using System; using System.Collections.Generic; namespace Observable { public class Unsubscriber<T> : IDisposable { private readonly List<IObserver<T>> m_Observers; private readonly IObserver<T> m_Observer; private bool m_IsDisposed; public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer) { m_Observers = observers; m_Observer = observer; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing && m_Observers.Contains(m_Observer)) { m_Observers.Remove(m_Observer); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
Ce que l'on peut remarquer ici :
IDisposable
en appliquant le Disposable Design Pattern .
using System; using System.Collections.Generic; namespace Observable { public class WeatherForecastUnsubscriber : Unsubscriber<WeatherInfo> { public WeatherForecastUnsubscriber( List<IObserver<WeatherInfo>> observers, IObserver<WeatherInfo> observer) : base(observers, observer) { } } }
Ce que l'on peut remarquer ici :
Unsubscriber<T>
.
using System; namespace Observable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
Ce que l'on peut remarquer ici :
WeatherForecastObserver
implémente IObserver<WeatherInfo>
.OnNext
, nous écrivons la température sur la console.OnCompleted
, nous écrivons « Completed » sur la console.OnError
, nous écrivons "Error" dans la console.void Subscribe(WeatherForecast provider)
pour permettre au module principal de déclencher le processus d'enregistrement. L'objet de désabonnement renvoyé est enregistré en interne pour être utilisé en cas de désabonnement.void Unsubscribe()
est définie et utilise l'objet de désabonnement enregistré en interne.
using System; namespace Observable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
Ce que l'on peut remarquer ici :
Lorsque j'ai vérifié l'implémentation de Microsoft, j'ai trouvé quelques problèmes. Par conséquent, j'ai décidé de faire quelques modifications mineures.
using System; using System.Collections.Generic; namespace ExtendedObservable { public interface IExtendedObservable<out T> : IObservable<T> { IReadOnlyCollection<T> Snapshot { get; } IDisposable Subscribe(IObserver<T> observer, bool withHistory); } }
Ce que l'on peut remarquer ici :
IExtendedObservable<out T>
étend l'interface IObservable<T>
.IReadOnlyCollection<T> Snapshot
pour permettre aux autres modules d'obtenir une liste instantanée des entrées d'informations déjà existantes sans avoir à s'abonner.IDisposable Subscribe(IObserver<T> observer, bool withHistory)
avec un paramètre supplémentaire bool withHistory
afin que l'observateur puisse décider s'il souhaite être informé des entrées d'informations déjà existantes ou non au moment de l'abonnement.
using System; namespace ExtendedObservable { public class Unsubscriber : IDisposable { private readonly Action m_UnsubscribeAction; private bool m_IsDisposed; public Unsubscriber(Action unsubscribeAction) { m_UnsubscribeAction = unsubscribeAction; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (m_IsDisposed) return; if (disposing) { m_UnsubscribeAction(); } m_IsDisposed = true; } ~Unsubscriber() { Dispose(false); } } }
Ce que l'on peut remarquer ici :
Unsubscriber
n'est pas générique.
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }
Ce que l'on peut remarquer ici :
<T>
de Unsubscriber<T>
.Action
à appeler en cas de suppression.
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecast : IExtendedObservable<WeatherInfo> { private readonly List<IObserver<WeatherInfo>> m_Observers; private readonly List<WeatherInfo> m_WeatherInfoList; public WeatherForecast() { m_Observers = new List<IObserver<WeatherInfo>>(); m_WeatherInfoList = new List<WeatherInfo>(); } public IReadOnlyCollection<WeatherInfo> Snapshot => m_WeatherInfoList; public IDisposable Subscribe(IObserver<WeatherInfo> observer) { return Subscribe(observer, false); } public IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) { if (!m_Observers.Contains(observer)) { m_Observers.Add(observer); if (withHistory) { foreach (var item in m_WeatherInfoList) { observer.OnNext(item); } } } return new WeatherForecastUnsubscriber( () => { if (m_Observers.Contains(observer)) { m_Observers.Remove(observer); } }); } public void RegisterWeatherInfo(WeatherInfo weatherInfo) { m_WeatherInfoList.Add(weatherInfo); foreach (var observer in m_Observers) { observer.OnNext(weatherInfo); } } public void ClearWeatherInfo() { m_WeatherInfoList.Clear(); } } }
Ce que l'on peut remarquer ici :
IReadOnlyCollection<WeatherInfo> Snapshot
qui renvoie la liste interne m_WeatherInfoList
mais comme IReadOnlyCollection
.IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
qui utilise le paramètre withHistory
.
using System; namespace ExtendedObservable { public class WeatherForecastObserver : IObserver<WeatherInfo> { private IDisposable m_Unsubscriber; public virtual void Subscribe(WeatherForecast provider) { m_Unsubscriber = provider.Subscribe(this, true); } public virtual void Unsubscribe() { m_Unsubscriber.Dispose(); } public void OnCompleted() { Console.WriteLine("Completed"); } public void OnError(Exception error) { Console.WriteLine("Error"); } public void OnNext(WeatherInfo value) { Console.WriteLine($"Temperature: {value.Temperature}"); } } }
Ce que nous pouvons remarquer ici, c'est que c'est presque la même chose sauf pour Subscribe(WeatherForecast provider)
qui décide maintenant s'il doit Subscribe
avec l'historique ou non.
using System; namespace ExtendedObservable { class Program { static void Main(string[] args) { var provider = new WeatherForecast(); provider.RegisterWeatherInfo(new WeatherInfo(1)); provider.RegisterWeatherInfo(new WeatherInfo(2)); provider.RegisterWeatherInfo(new WeatherInfo(3)); var observer = new WeatherForecastObserver(); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(4)); provider.RegisterWeatherInfo(new WeatherInfo(5)); observer.Unsubscribe(); provider.RegisterWeatherInfo(new WeatherInfo(6)); observer.Subscribe(provider); provider.RegisterWeatherInfo(new WeatherInfo(7)); Console.ReadLine(); } } }
C'est la même chose qu'avant.
Vous connaissez maintenant les bases du modèle de conception Observer dans .NET C#. Cependant, ce n'est pas la fin de l'histoire.
Il existe des bibliothèques construites au-dessus des interfaces IObservable<T>
et IObserver<T>
fournissant des fonctionnalités et des capacités plus intéressantes que vous pourriez trouver utiles.
L'une de ces bibliothèques est la
Par conséquent, je vous encourage à explorer ces bibliothèques et à les essayer. Je suis sûr que vous aimeriez certains d'entre eux.
Également publié ici.