paint-brush
Un guide essentiel du modèle de conception Observer dans .NET C#par@ahmedtarekhasan
3,303 lectures
3,303 lectures

Un guide essentiel du modèle de conception Observer dans .NET C#

par Ahmed Tarek Hasan19m2023/04/10
Read on Terminal Reader

Trop long; Pour lire

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 permet à un abonné de s'inscrire et de recevoir des notifications d'un fournisseur. Il convient à tous les scénarios nécessitant une notification push. Ce qui rend le motif unique, c'est qu'en l'utilisant, vous pouvez y parvenir sans avoir une relation étroitement couplée.
featured image - Un guide essentiel du modèle de conception Observer dans .NET C#
Ahmed Tarek Hasan HackerNoon profile picture
0-item

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 Documentation de Microsoft :


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 :

  1. Nous avons deux parties ou modules.
  2. 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).
  3. Le module qui s'intéresse à un flux d'informations venant d'ailleurs. Ce module s'appelle Observer (car il observe des informations).

Photo de Den Harrson sur Unsplash

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 :

  1. L' Observable connaît les informations minimales nécessaires sur l' Observer .
  2. L' observateur connaît les informations minimales nécessaires sur l' observable .
  3. Même la connaissance mutuelle est obtenue par des abstractions, et non par des implémentations concrètes.
  4. À la fin, les deux modules peuvent faire leur travail, et seulement leur travail.

Photo de Lucas Santos sur Unsplash

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 Covariance et contravariance dans .NET C# .


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 Covariance et contravariance dans .NET C# .


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.


Photo de Tadas Sar sur Unsplash

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 :

  1. La classe WeatherForecast implémente IObservable<WeatherInfo> .
  2. 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 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.
  3. Enfin, nous renvoyons une nouvelle instance de la classe WeatherForecastUnsubscriber à utiliser par l'observateur pour se désabonner du flux d'informations.
  4. La méthode 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.



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 :

  1. Il s'agit d'une classe de base pour tout désabonné.
  2. Il implémente IDisposable en appliquant le Disposable Design Pattern .
  3. Par l'intermédiaire du constructeur, il prend en compte la liste complète des observateurs et l'observateur pour lequel il est créé.
  4. 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 :

  1. Ceci hérite de la classe Unsubscriber<T> .
  2. 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 :

  1. La classe WeatherForecastObserver implémente IObserver<WeatherInfo> .
  2. Sur la méthode OnNext , nous écrivons la température sur la console.
  3. Sur la méthode OnCompleted , nous écrivons « Completed » sur la console.
  4. Sur la méthode OnError , nous écrivons "Error" dans la console.
  5. 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.
  6. 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 :

  1. Nous avons créé une instance du fournisseur.
  2. Puis enregistré 3 éléments d'information.
  3. Jusqu'à ce moment, rien ne doit être enregistré sur la console car aucun observateur n'est défini.
  4. Puis créé une instance de l'observateur.
  5. Puis a abonné l'observateur au flux.
  6. 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.
  7. Ensuite, nous enregistrons 2 informations.
  8. Nous obtenons donc 2 autres messages enregistrés dans la console.
  9. Puis on se désabonne.
  10. Ensuite, nous enregistrons 1 élément d'information.
  11. Cependant, cette information ne serait pas enregistrée dans la console car l'observateur s'était déjà désabonné.
  12. Ensuite, l'observateur s'abonne à nouveau.
  13. Ensuite, nous enregistrons 1 élément d'information.
  14. Ainsi, cette information est enregistrée dans la console.


Enfin, l'exécution de ceci devrait aboutir à ce résultat :


Image par Ahmed Tarek


Photo de Bruno Yamazaky sur Unsplash

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 :

  1. L'interface IExtendedObservable<out T> étend l'interface IObservable<T> .
  2. C'est Covariant . Si vous voulez en savoir plus à ce sujet, vous pouvez consulter l'article Covariance et contravariance dans .NET C# .
  3. 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.
  4. Nous avons également défini la méthode 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.



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 :

  1. Maintenant, la classe Unsubscriber n'est pas générique.
  2. C'est parce qu'il n'a plus besoin de connaître le type de l'entité d'information.
  3. 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.
  4. 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 :

  1. Nous avons supprimé la partie <T> de Unsubscriber<T> .
  2. 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 :

  1. C'est presque la même chose sauf pour la propriété IReadOnlyCollection<WeatherInfo> Snapshot qui renvoie la liste interne m_WeatherInfoList mais comme IReadOnlyCollection .
  2. Et la méthode IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) qui utilise le paramètre withHistory .



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 :


Image par Ahmed Tarek


Photo par Emily Morter sur Unsplash, ajustée par Ahmed Tarek

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 Extensions réactives pour .NET (Rx) bibliothèque. Il se compose d'un ensemble de méthodes d'extension et d'opérateurs de séquence standard LINQ pour prendre en charge la programmation asynchrone.


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.