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äß
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:
- Wir haben zwei Parteien bzw. Module.
- 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).
- Das Modul, das an einem Informationsstrom interessiert ist, der von woanders kommt. Dieses Modul wird Observer genannt (da es Informationen beobachtet).
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:
- Das Observable kennt die minimal benötigten Informationen über den Observer .
- Der Observer kennt die minimalen Informationen, die über das Observable benötigt werden.
- Sogar die gegenseitige Kenntnis wird durch Abstraktionen erreicht, nicht durch konkrete Implementierungen.
- Am Ende können beide Module ihren Job machen, und zwar nur ihren Job.
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
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
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.
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:
- Die Klasse
WeatherForecast
implementiertIObservable<WeatherInfo>
. - 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 lokalenm_Observers
Beobachterliste hinzu. Dann durchlaufen wir nacheinander alleWeatherInfo
Einträge in der lokalenm_WeatherInfoList
Liste und informieren den Observer darüber, indem wir dieOnNext
Methode des Observers aufrufen. - Schließlich geben wir eine neue Instanz der
WeatherForecastUnsubscriber
Klasse zurück, die vom Observer zum Abbestellen des Informationsstroms verwendet wird. - Die
RegisterWeatherInfo
Methode ist so definiert, dass das Hauptmodul neueWeatherInfo
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:
- Dies ist eine Basisklasse für jeden Un-Subscriber.
- Es implementiert
IDisposable
durch Anwenden des „Disposable Design Pattern“ . - Über den Konstruktor erhält es die vollständige Liste der Beobachter und des Beobachters, für den es erstellt wurde.
- 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:
- Dies erbt von der
Unsubscriber<T>
-Klasse. - 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:
- Die Klasse
WeatherForecastObserver
implementiertIObserver<WeatherInfo>
. - Bei der
OnNext
-Methode schreiben wir die Temperatur in die Konsole. - Bei der
OnCompleted
Methode schreiben wir „Completed“ in die Konsole. - Bei der
OnError
Methode schreiben wir „Error“ in die Konsole. - 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. - 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:
- Wir haben eine Instanz des Anbieters erstellt.
- Dann wurden 3 Informationen registriert.
- Bis zu diesem Zeitpunkt sollte nichts an der Konsole protokolliert werden, da keine Beobachter definiert sind.
- Anschließend wurde eine Instanz des Beobachters erstellt.
- Anschließend abonniert der Beobachter den Stream.
- 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.
- Dann registrieren wir 2 Informationen.
- Wir erhalten also zwei weitere Nachrichten, die in der Konsole protokolliert werden.
- Dann melden wir uns ab.
- Dann registrieren wir 1 Information.
- Diese Information wurde jedoch nicht in der Konsole protokolliert, da sich der Beobachter bereits abgemeldet hatte.
- Dann abonniert der Beobachter erneut.
- Dann registrieren wir 1 Information.
- Diese Informationen werden also in der Konsole protokolliert.
Abschließend sollte die Ausführung dieses Befehls zu diesem Ergebnis führen:
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:
- Die
IExtendedObservable<out T>
-Schnittstelle erweitert dieIObservable<T>
-Schnittstelle. - Es ist kovariant . Wenn Sie mehr darüber erfahren möchten, können Sie den Artikel lesen
Kovarianz und Kontravarianz in .NET C# . - 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. - Wir haben auch die Methode
IDisposable Subscribe(IObserver<T> observer, bool withHistory)
mit einem zusätzlichenbool 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:
- Nun ist die
Unsubscriber
Klasse nicht generisch. - Dies liegt daran, dass es nicht mehr erforderlich ist, den Typ der Info-Entität zu kennen.
- 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.
- 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:
- Wir haben den Teil
<T>
ausUnsubscriber<T>
entfernt. - 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:
- Es ist fast dasselbe, mit Ausnahme der
IReadOnlyCollection<WeatherInfo> Snapshot
Eigenschaft, die die internem_WeatherInfoList
Liste zurückgibt, jedoch alsIReadOnlyCollection
. - Und die Methode
IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
, die den ParameterwithHistory
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:
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
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.