paint-brush
Ein wesentlicher Leitfaden zum Observer-Entwurfsmuster in .NET C#by@ahmedtarekhasan
3,180
3,180

Ein wesentlicher Leitfaden zum Observer-Entwurfsmuster in .NET C#

Ahmed Tarek Hasan19m2023/04/10
Read on Terminal Reader
Read this story w/o Javascript

In diesem Artikel erfahren Sie mehr über das Observer Design Pattern in.NET C# mit einigen Verbesserungen. Das Beobachter-Entwurfsmuster ermöglicht es einem Abonnenten, sich bei einem Anbieter zu registrieren und Benachrichtigungen von ihm zu erhalten. Es eignet sich für jedes Szenario, das eine Push-basierte Benachrichtigung erfordert. Das Besondere an dem Muster ist, dass Sie dies erreichen können, ohne eine eng gekoppelte Beziehung zu haben.
featured image - Ein wesentlicher Leitfaden zum Observer-Entwurfsmuster in .NET C#
Ahmed Tarek Hasan HackerNoon profile picture
0-item

In diesem Artikel erfahren Sie mehr über das Observer Design Pattern in .NET C# mit einigen Verbesserungen.


Definition des Beobachter-Entwurfsmusters

Das Observer Design Pattern ist eines der wichtigsten und am häufigsten verwendeten Design Patterns.


Schauen wir uns zunächst die formale Definition des Observer Design Pattern an.


Gemäß Dokumentation von Microsoft :


Das Beobachter-Entwurfsmuster ermöglicht es einem Abonnenten, sich bei einem Anbieter zu registrieren und Benachrichtigungen von ihm zu erhalten. Es eignet sich für jedes Szenario, das eine Push-basierte Benachrichtigung erfordert. Das Muster definiert einen Anbieter (auch bekannt als Subjekt oder Observable) und null, einen oder mehrere Beobachter. Beobachter registrieren sich beim Anbieter, und wenn eine vordefinierte Bedingung, ein vordefiniertes Ereignis oder eine Zustandsänderung eintritt, benachrichtigt der Anbieter automatisch alle Beobachter, indem er eine ihrer Methoden aufruft. In diesem Methodenaufruf kann der Anbieter Beobachtern auch aktuelle Zustandsinformationen zur Verfügung stellen. In .NET wird das Beobachterentwurfsmuster durch die Implementierung der generischen Schnittstellen System.IObservable<T> und System.IObserver<T> angewendet. Der generische Typparameter stellt den Typ dar, der Benachrichtigungsinformationen bereitstellt.


Aus der obigen Definition können wir also Folgendes verstehen:

  1. Wir haben zwei Parteien bzw. Module.
  2. Das Modul, das einen Informationsstrom bereitstellt. Dieses Modul heißt Provider (da es Informationen bereitstellt), oder Subject (da es Informationen an die Außenwelt weitergibt) oder Observable (da es von der Außenwelt beobachtet werden könnte).
  3. Das Modul, das an einem Informationsstrom interessiert ist, der von woanders kommt. Dieses Modul wird Observer genannt (da es Informationen beobachtet).

Foto von Den Harrson auf Unsplash

Vorteile des Observer Design Pattern

Wie wir jetzt wissen, formuliert das Observer Design Pattern die Beziehung zwischen den Observable- und Observer- Modulen. Das Besondere am Observer Design Pattern ist, dass Sie dies erreichen können, ohne eine eng gekoppelte Beziehung zu haben.


Wenn Sie die Funktionsweise des Musters analysieren, würden Sie Folgendes finden:

  1. Das Observable kennt die minimal benötigten Informationen über den Observer .
  2. Der Observer kennt die minimalen Informationen, die über das Observable benötigt werden.
  3. Sogar die gegenseitige Kenntnis wird durch Abstraktionen erreicht, nicht durch konkrete Implementierungen.
  4. Am Ende können beide Module ihren Job machen, und zwar nur ihren Job.

Foto von Lucas Santos auf Unsplash

Verwendete Abstraktionen

Dies sind die Abstraktionen , die zur Implementierung des Observer Design Pattern in .NET C# verwendet werden.



IObservable<out T>

Dies ist eine kovariante Schnittstelle, die jedes Observable darstellt. Wenn Sie mehr über Varianz in .NET erfahren möchten, können Sie den Artikel lesen Kovarianz und Kontravarianz in .NET C# .


In dieser Schnittstelle definierte Mitglieder sind:


 public IDisposable Subscribe (IObserver<out T> observer);


Die Subscribe Methode sollte aufgerufen werden, um das Observable darüber zu informieren, dass ein Observer an seinem Informationsstrom interessiert ist.


Die Subscribe Methode gibt ein Objekt zurück, das die IDisposable Schnittstelle implementiert. Dieses Objekt könnte dann vom Observer verwendet werden, um sich vom Informationsstrom abzumelden, der vom Observable bereitgestellt wird. Sobald dies erledigt ist, wird der Beobachter nicht über Aktualisierungen des Informationsstroms benachrichtigt.



IObserver<in T>

Dies ist eine kontravariante Schnittstelle, die jeden Observer darstellt. Wenn Sie mehr über Varianz in .NET erfahren möchten, können Sie den Artikel lesen Kovarianz und Kontravarianz in .NET C# .


In dieser Schnittstelle definierte Mitglieder sind:


 public void OnCompleted (); public void OnError (Exception error); public void OnNext (T value);


Die OnCompleted Methode sollte vom Observable aufgerufen werden, um den Observer darüber zu informieren, dass der Informationsstrom abgeschlossen ist und der Observer keine weiteren Informationen erwarten sollte.


Die OnError Methode sollte vom Observable aufgerufen werden, um den Observer darüber zu informieren, dass ein Fehler aufgetreten ist.


Die OnNext Methode sollte vom Observable aufgerufen werden, um den Observer darüber zu informieren, dass eine neue Information bereit ist und dem Stream hinzugefügt wird.


Foto von Tadas Sar auf Unsplash

Microsoft-Implementierung

Sehen wir uns nun an, wie Microsoft die Implementierung des Observer Design Pattern in C# empfiehlt. Später werde ich Ihnen einige kleinere Verbesserungen zeigen, die ich selbst implementiert habe.


Wir werden eine einfache Wettervorhersage-Konsolenanwendung erstellen. In dieser Anwendung verfügen wir über das WeatherForecast- Modul (Observable, Provider, Subject) und das WeatherForecastObserver- Modul (Observer).


Beginnen wir also mit der Betrachtung der Implementierung.



WetterInfo

 namespace Observable { public class WeatherInfo { internal WeatherInfo(double temperature) { Temperature = temperature; } public double Temperature { get; } } }


Dies ist die Entität, die die Information darstellt, die im Informationsstrom fließen soll.



Wettervorhersage

 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(); } } }


Was wir hier bemerken können:

  1. Die Klasse WeatherForecast implementiert IObservable<WeatherInfo> .
  2. Bei der Implementierung der Subscribe Methode prüfen wir, ob der übergebene Observer bereits zuvor registriert wurde oder nicht. Wenn nicht, fügen wir es der lokalen m_Observers Beobachterliste hinzu. Dann durchlaufen wir nacheinander alle WeatherInfo Einträge in der lokalen m_WeatherInfoList Liste und informieren den Observer darüber, indem wir die OnNext Methode des Observers aufrufen.
  3. Schließlich geben wir eine neue Instanz der WeatherForecastUnsubscriber Klasse zurück, die vom Observer zum Abbestellen des Informationsstroms verwendet wird.
  4. Die RegisterWeatherInfo Methode ist so definiert, dass das Hauptmodul neue WeatherInfo registrieren kann. In der realen Welt könnte dies durch einen internen geplanten API-Aufruf oder einen Listener für einen SignalR-Hub oder etwas anderes ersetzt werden, das als Informationsquelle fungiert.



Abmelder<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); } } }


Was wir hier bemerken können:

  1. Dies ist eine Basisklasse für jeden Un-Subscriber.
  2. Es implementiert IDisposable durch Anwenden des „Disposable Design Pattern“ .
  3. Über den Konstruktor erhält es die vollständige Liste der Beobachter und des Beobachters, für den es erstellt wurde.
  4. Beim Entsorgen wird geprüft, ob der Beobachter bereits in der vollständigen Liste der Beobachter vorhanden ist. Wenn ja, wird es aus der Liste entfernt.



WeatherForecastUnsubscriber

 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) { } } }


Was wir hier bemerken können:

  1. Dies erbt von der Unsubscriber<T> -Klasse.
  2. Es findet keine besondere Behandlung statt.



WeatherForecastObserver

 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}"); } } }


Was wir hier bemerken können:

  1. Die Klasse WeatherForecastObserver implementiert IObserver<WeatherInfo> .
  2. Bei der OnNext -Methode schreiben wir die Temperatur in die Konsole.
  3. Bei der OnCompleted Methode schreiben wir „Completed“ in die Konsole.
  4. Bei der OnError Methode schreiben wir „Error“ in die Konsole.
  5. Wir haben die Methode void Subscribe(WeatherForecast provider) definiert, damit das Hauptmodul den Registrierungsprozess auslösen kann. Das zurückgegebene Abmeldeobjekt wird intern gespeichert, um im Falle einer Abmeldung verwendet zu werden.
  6. Mit dem gleichen Konzept wird die Methode void Unsubscribe() definiert und nutzt das intern gespeicherte Un-Subscriber-Objekt.



Programm

 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(); } } }


Was wir hier bemerken können:

  1. Wir haben eine Instanz des Anbieters erstellt.
  2. Dann wurden 3 Informationen registriert.
  3. Bis zu diesem Zeitpunkt sollte nichts an der Konsole protokolliert werden, da keine Beobachter definiert sind.
  4. Anschließend wurde eine Instanz des Beobachters erstellt.
  5. Anschließend abonniert der Beobachter den Stream.
  6. In diesem Moment sollten wir in der Konsole 3 protokollierte Temperaturen finden. Dies liegt daran, dass der Beobachter beim Abonnieren über die bereits vorhandenen Informationen benachrichtigt wird. In unserem Fall handelt es sich um drei Informationen.
  7. Dann registrieren wir 2 Informationen.
  8. Wir erhalten also zwei weitere Nachrichten, die in der Konsole protokolliert werden.
  9. Dann melden wir uns ab.
  10. Dann registrieren wir 1 Information.
  11. Diese Information wurde jedoch nicht in der Konsole protokolliert, da sich der Beobachter bereits abgemeldet hatte.
  12. Dann abonniert der Beobachter erneut.
  13. Dann registrieren wir 1 Information.
  14. Diese Informationen werden also in der Konsole protokolliert.


Abschließend sollte die Ausführung dieses Befehls zu diesem Ergebnis führen:


Bild von Ahmed Tarek


Foto von Bruno Yamazaky auf Unsplash

Meine erweiterte Implementierung

Als ich die Implementierung von Microsoft überprüfte, stieß ich auf einige Bedenken. Deshalb habe ich beschlossen, einige kleinere Änderungen vorzunehmen.


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); } }


Was wir hier bemerken können:

  1. Die IExtendedObservable<out T> -Schnittstelle erweitert die IObservable<T> -Schnittstelle.
  2. Es ist kovariant . Wenn Sie mehr darüber erfahren möchten, können Sie den Artikel lesen Kovarianz und Kontravarianz in .NET C# .
  3. Wir haben IReadOnlyCollection<T> Snapshot definiert, um anderen Modulen zu ermöglichen, sofort eine Liste bereits vorhandener Infoeinträge abzurufen, ohne sich anmelden zu müssen.
  4. Wir haben auch die Methode IDisposable Subscribe(IObserver<T> observer, bool withHistory) mit einem zusätzlichen bool withHistory Parameter definiert, damit der Observer zum Zeitpunkt des Abonnements entscheiden kann, ob er über die bereits vorhandenen Infoeinträge benachrichtigt werden möchte oder nicht.



Abmelden

 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); } } }


Was wir hier bemerken können:

  1. Nun ist die Unsubscriber Klasse nicht generisch.
  2. Dies liegt daran, dass es nicht mehr erforderlich ist, den Typ der Info-Entität zu kennen.
  3. Anstatt Zugriff auf die vollständige Liste der Beobachter und des Beobachters zu haben, für den es erstellt wurde, benachrichtigt es das Observable lediglich, wenn es entsorgt wird, und das Observable führt den Abmeldevorgang selbst durch.
  4. Auf diese Weise leistet es weniger als zuvor und erledigt nur seine Aufgabe.



WeatherForecastUnsubscriber

 using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }


Was wir hier bemerken können:

  1. Wir haben den Teil <T> aus Unsubscriber<T> entfernt.
  2. Und jetzt übernimmt der Konstruktor eine Action , die im Falle einer Entsorgung aufgerufen wird.



Wettervorhersage

 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(); } } }


Was wir hier bemerken können:

  1. Es ist fast dasselbe, mit Ausnahme der IReadOnlyCollection<WeatherInfo> Snapshot Eigenschaft, die die interne m_WeatherInfoList Liste zurückgibt, jedoch als IReadOnlyCollection .
  2. Und die Methode IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory) , die den Parameter withHistory verwendet.



WeatherForecastObserver

 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}"); } } }


Was wir hier bemerken können, ist, dass es fast dasselbe ist, mit Ausnahme von Subscribe(WeatherForecast provider) , das nun entscheidet, ob ein Subscribe mit Verlauf erfolgen soll oder nicht.



Programm

 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(); } } }


Es ist das Gleiche wie zuvor.




Abschließend sollte die Ausführung dieses Befehls zum gleichen Ergebnis wie zuvor führen:


Bild von Ahmed Tarek


Foto von Emily Morter auf Unsplash, angepasst von Ahmed Tarek

Was kommt als nächstes

Jetzt kennen Sie die Grundlagen des Observer Design Pattern in .NET C#. Dies ist jedoch nicht das Ende der Geschichte.


Es gibt Bibliotheken, die auf den Schnittstellen IObservable<T> und IObserver<T> aufbauen und weitere coole Features und Fähigkeiten bieten, die Sie möglicherweise nützlich finden.


Eine dieser Bibliotheken ist die Reaktive Erweiterungen für .NET (Rx) Bibliothek. Es besteht aus einer Reihe von Erweiterungsmethoden und LINQ-Standardsequenzoperatoren zur Unterstützung der asynchronen Programmierung.


Deshalb empfehle ich Ihnen, diese Bibliotheken zu erkunden und auszuprobieren. Ich bin mir sicher, dass Ihnen einige davon gefallen würden.


Auch hier veröffentlicht.