Dans cet article, vous découvrirez le modèle de conception Observer dans .NET C# avec quelques améliorations.
Définition du modèle de conception d'observateur
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.
Ainsi, à partir de la définition ci-dessus, nous pouvons comprendre ce qui suit :
- Nous avons deux parties ou modules.
- Le module qui a un flux d'informations à fournir. Ce module est appelé Fournisseur (car il fournit des informations), ou Sujet (car il soumet des informations au monde extérieur), ou Observable (car il pourrait être observé par le monde extérieur).
- Le module qui s'intéresse à un flux d'informations venant d'ailleurs. Ce module s'appelle Observer (car il observe des informations).
Avantages du modèle de conception d'observateur
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 :
- L' Observable connaît les informations minimales nécessaires sur l' Observer .
- L' observateur connaît les informations minimales nécessaires sur l' observable .
- Même la connaissance mutuelle est obtenue par des abstractions, et non par des implémentations concrètes.
- À la fin, les deux modules peuvent faire leur travail, et seulement leur travail.
Abstractions utilisées
Ce sont les abstractions utilisées pour implémenter le modèle de conception Observer dans .NET C# .
IObservable<hors T>
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.
IObserver<en T>
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.
Implémentation de Microsoft
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.
InfoMétéo
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.
Prévisions météorologiques
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 :
- La classe
WeatherForecast
implémenteIObservable<WeatherInfo>
. - Dans l'implémentation de la méthode
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 observateursm_Observers
. Ensuite, nous bouclons sur toutes les entréesWeatherInfo
que nous avons dans la liste localem_WeatherInfoList
une par une et en informons l'Observer en appelant la méthodeOnNext
de l'Observer. - Enfin, nous renvoyons une nouvelle instance de la classe
WeatherForecastUnsubscriber
à utiliser par l'observateur pour se désabonner du flux d'informations. - La méthode
RegisterWeatherInfo
est définie pour que le module principal puisse enregistrer de nouvellesWeatherInfo
. 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.
Désabonnement<T>
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 :
- Il s'agit d'une classe de base pour tout désabonné.
- Il implémente
IDisposable
en appliquant le Disposable Design Pattern . - Par l'intermédiaire du constructeur, il prend en compte la liste complète des observateurs et l'observateur pour lequel il est créé.
- Lors de la suppression, il vérifie si l'observateur existe déjà dans la liste complète des observateurs. Si oui, il le supprime de la liste.
WeatherForecastDésabonnement
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 :
- Ceci hérite de la classe
Unsubscriber<T>
. - Aucune manipulation particulière n'est effectuée.
PrévisionsMétéoObservateur
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 :
- La classe
WeatherForecastObserver
implémenteIObserver<WeatherInfo>
. - Sur la méthode
OnNext
, nous écrivons la température sur la console. - Sur la méthode
OnCompleted
, nous écrivons « Completed » sur la console. - Sur la méthode
OnError
, nous écrivons "Error" dans la console. - Nous avons défini la méthode
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. - En utilisant le même concept, la méthode
void Unsubscribe()
est définie et utilise l'objet de désabonnement enregistré en interne.
Programme
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 :
- Nous avons créé une instance du fournisseur.
- Puis enregistré 3 éléments d'information.
- Jusqu'à ce moment, rien ne doit être enregistré sur la console car aucun observateur n'est défini.
- Puis créé une instance de l'observateur.
- Puis a abonné l'observateur au flux.
- A ce moment, nous devrions trouver 3 températures enregistrées dans la console. En effet, lorsque l'observateur s'abonne, il est informé des informations déjà existantes et dans notre cas, il s'agit de 3 informations.
- Ensuite, nous enregistrons 2 informations.
- Nous obtenons donc 2 autres messages enregistrés dans la console.
- Puis on se désabonne.
- Ensuite, nous enregistrons 1 élément d'information.
- Cependant, cette information ne serait pas enregistrée dans la console car l'observateur s'était déjà désabonné.
- Ensuite, l'observateur s'abonne à nouveau.
- Ensuite, nous enregistrons 1 élément d'information.
- Ainsi, cette information est enregistrée dans la console.
Enfin, l'exécution de ceci devrait aboutir à ce résultat :
Ma mise en œuvre étendue
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.
IExtendedObservable<out T>
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 :
- L'interface
IExtendedObservable<out T>
étend l'interfaceIObservable<T>
. - C'est Covariant . Si vous voulez en savoir plus à ce sujet, vous pouvez consulter l'article
Covariance et contravariance dans .NET C# . - Nous avons défini la proprié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. - Nous avons également défini la méthode
IDisposable Subscribe(IObserver<T> observer, bool withHistory)
avec un paramètre supplémentairebool 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.
Désabonnement
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 :
- Maintenant, la classe
Unsubscriber
n'est pas générique. - C'est parce qu'il n'a plus besoin de connaître le type de l'entité d'information.
- Au lieu d'avoir accès à la liste complète des observateurs et à l'observateur pour lequel il a été créé, il notifie simplement l'observable lorsqu'il est supprimé et l'observable gère lui-même le processus de désinscription.
- De cette façon, il en fait moins qu'avant et il ne fait que son travail.
WeatherForecastDésabonnement
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 :
- Nous avons supprimé la partie
<T>
deUnsubscriber<T>
. - Et maintenant, le constructeur prend une
Action
à appeler en cas de suppression.
Prévisions météorologiques
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 :
- C'est presque la même chose sauf pour la propriété
IReadOnlyCollection<WeatherInfo> Snapshot
qui renvoie la liste internem_WeatherInfoList
mais commeIReadOnlyCollection
. - Et la méthode
IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
qui utilise le paramètrewithHistory
.
PrévisionsMétéoObservateur
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.
Programme
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.
Enfin, l'exécution de ceci devrait aboutir au même résultat qu'avant :
Et après
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.