In diesem Artikel erfahren Sie mehr über das Observer Design Pattern in .NET C# mit einigen Verbesserungen.
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.
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:
Dies sind die Abstraktionen , die zur Implementierung des Observer Design Pattern in .NET C# verwendet werden.
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.
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.
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.
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.
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:
WeatherForecast
implementiert IObservable<WeatherInfo>
.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.WeatherForecastUnsubscriber
Klasse zurück, die vom Observer zum Abbestellen des Informationsstroms verwendet wird.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.
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:
IDisposable
durch Anwenden des „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) { } } }
Was wir hier bemerken können:
Unsubscriber<T>
-Klasse.
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:
WeatherForecastObserver
implementiert IObserver<WeatherInfo>
.OnNext
-Methode schreiben wir die Temperatur in die Konsole.OnCompleted
Methode schreiben wir „Completed“ in die Konsole.OnError
Methode schreiben wir „Error“ in die Konsole.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.void Unsubscribe()
definiert und nutzt das intern gespeicherte Un-Subscriber-Objekt.
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:
Als ich die Implementierung von Microsoft überprüfte, stieß ich auf einige Bedenken. Deshalb habe ich beschlossen, einige kleinere Änderungen vorzunehmen.
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:
IExtendedObservable<out T>
-Schnittstelle erweitert die IObservable<T>
-Schnittstelle.IReadOnlyCollection<T> Snapshot
definiert, um anderen Modulen zu ermöglichen, sofort eine Liste bereits vorhandener Infoeinträge abzurufen, ohne sich anmelden zu müssen.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.
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:
Unsubscriber
Klasse nicht generisch.
using System; using System.Collections.Generic; namespace ExtendedObservable { public class WeatherForecastUnsubscriber : Unsubscriber { public WeatherForecastUnsubscriber( Action unsubscribeAction) : base(unsubscribeAction) { } } }
Was wir hier bemerken können:
<T>
aus Unsubscriber<T>
entfernt.Action
, die im Falle einer Entsorgung aufgerufen wird.
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:
IReadOnlyCollection<WeatherInfo> Snapshot
Eigenschaft, die die interne m_WeatherInfoList
Liste zurückgibt, jedoch als IReadOnlyCollection
.IDisposable Subscribe(IObserver<WeatherInfo> observer, bool withHistory)
, die den Parameter withHistory
verwendet.
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.
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.
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.