Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Das Beobachtermuster ermöglicht es einem Abonnenten, sich bei einem Anbieter zu registrieren und Benachrichtigungen zu empfangen. Es eignet sich für jedes Szenario, für das pushbasierte Benachrichtigungen erforderlich sind. Das Muster definiert einen Anbieter (auch als Betreff oder feststellbar bezeichnet) und Null, einen oder mehrere Beobachter. Beobachter registrieren sich beim Anbieter, und wenn eine vordefinierte Bedingung, ein Ereignis oder eine Zustandsänderung eintritt, benachrichtigt der Anbieter automatisch alle Beobachter durch Aufrufen einer Stellvertretung. In diesem Methodenaufruf kann der Anbieter auch aktuelle Statusinformationen für Beobachter bereitstellen. In .NET wird das Beobachterentwurfsmuster angewendet, indem die generischen System.IObservable<T> Und System.IObserver<T> Schnittstellen implementiert werden. Der generische Typparameter stellt den Typ dar, der Benachrichtigungsinformationen bereitstellt.
Wann das Muster angewendet werden soll
Das Designmuster des Beobachters eignet sich für verteilte pushbasierte Benachrichtigungen, da es eine klare Trennung zwischen zwei verschiedenen Komponenten oder Anwendungsebenen unterstützt, z. B. eine Datenquellenebene (Geschäftslogik) und eine Benutzeroberfläche (Anzeigeebene). Das Muster kann implementiert werden, wenn ein Anbieter Rückrufe verwendet, um seine Clients mit aktuellen Informationen zu versorgen.
Für die Implementierung des Musters müssen Sie die folgenden Details angeben:
Ein Anbieter oder Subjekt, das das Objekt ist, das Benachrichtigungen an Beobachter sendet. Ein Anbieter ist eine Klasse oder Struktur, die die IObservable<T> Schnittstelle implementiert. Der Anbieter muss eine einzige Methode implementieren, IObservable<T>.Subscribedie von Beobachtern aufgerufen wird, die Benachrichtigungen vom Anbieter erhalten möchten.
Ein Beobachter, bei dem es sich um ein Objekt handelt, das Benachrichtigungen von einem Anbieter empfängt. Ein Beobachter ist eine Klasse oder Struktur, die die IObserver<T> Schnittstelle implementiert. Der Beobachter muss drei Methoden implementieren, die alle vom Anbieter aufgerufen werden:
- IObserver<T>.OnNext, das den Beobachter mit neuen oder aktuellen Informationen versorgt.
- IObserver<T>.OnError, der den Beobachter darüber informiert, dass ein Fehler aufgetreten ist.
- IObserver<T>.OnCompleted, was angibt, dass der Anbieter das Senden von Benachrichtigungen abgeschlossen hat.
Ein Mechanismus, der es dem Anbieter ermöglicht, Beobachter nachzuverfolgen. In der Regel verwendet der Anbieter ein Containerobjekt, z. B. ein System.Collections.Generic.List<T> Objekt, um Verweise auf die IObserver<T> Implementierungen zu speichern, die Benachrichtigungen abonniert haben. Durch das Verwenden eines Speichercontainers zu diesem Zweck kann der Anbieter eine beliebige Anzahl von Beobachtern behandeln. Die Reihenfolge, in der Beobachter Benachrichtigungen empfangen, ist nicht definiert; Der Anbieter kann jede Methode verwenden, um die Bestellung zu bestimmen.
Eine IDisposable Implementierung, mit der der Anbieter Beobachter entfernen kann, wenn die Benachrichtigung abgeschlossen ist. Beobachter erhalten einen Verweis auf die IDisposable Implementierung aus der Subscribe Methode, damit sie auch die IDisposable.Dispose Methode aufrufen können, um das Abonnement abbestellen zu können, bevor der Anbieter das Senden von Benachrichtigungen abgeschlossen hat.
Ein Objekt, das die Daten enthält, die der Anbieter an seine Beobachter sendet. Der Typ dieses Objekts entspricht dem generischen Typparameter der IObservable<T> Und IObserver<T> Schnittstellen. Obwohl dieses Objekt mit der IObservable<T> Implementierung identisch sein kann, ist es meist ein separater Typ.
Hinweis
Zusätzlich zur Implementierung des Beobachterentwurfsmusters könnten Sie daran interessiert sein, Bibliotheken zu erkunden, die unter Verwendung der Schnittstellen IObservable<T> und IObserver<T> erstellt wurden. Reaktive Erweiterungen für .NET (Rx) bestehen beispielsweise aus einer Reihe von Erweiterungsmethoden und LINQ-Standardsequenzoperatoren zur Unterstützung der asynchronen Programmierung.
Wann Sollten Sie Alternativen in Betracht ziehen?
Die schnittstellen IObservable<T>/IObserver<T> eignen sich gut für Push-basierte Benachrichtigungsszenarien, aber .NET bietet andere Muster, die möglicherweise besser geeignet sind:
- Standard-.NET-Ereignisse – Für einfache Benachrichtigungsszenarien innerhalb einer einzelnen Anwendung sind events idiomatischer und einfacher zu implementieren.
-
IAsyncEnumerable<T>– Verwenden Sie für asynchrone Pull-basierte Sequenzen, bei denen der Verbraucher das Tempo steuert, asynchrone Datenströme. -
System.Threading.Channels— Für Producer-Consumer-Muster mit Backpressure und asynchroner Unterstützung verwenden Sie System.Threading.Channels. -
Reactive Extensions (Rx.NET) – Verwenden Sie für komplexe Ereigniskomposition, Filterung und Transformation das
System.Reactive-Paket, anstattIObservable<T>direkt zu implementieren.
Die prominenteste Verwendung von IObservable<T> in .NET ist DiagnosticListener, mit dem Framework- und Bibliotheksautoren strukturierte Diagnoseereignisse ausgeben können, die Nutzer abonnieren können.
Implementieren des Musters
Im folgenden Beispiel wird das Beobachterentwurfsmuster verwendet, um ein Informationssystem für Flughafengepäckansprüche zu implementieren. Eine BaggageInfo-Klasse stellt Informationen über ankommende Flüge und über die Laufbänder bereit, von denen das Gepäck der einzelnen Flüge abgeholt werden kann. Es wird im folgenden Beispiel gezeigt.
namespace Observables.Example;
public readonly record struct BaggageInfo(
int FlightNumber,
string From,
int Carousel);
Namespace Example
Public Structure BaggageInfo
Implements IEquatable(Of BaggageInfo)
Public ReadOnly Property FlightNumber As Integer
Public ReadOnly Property From As String
Public ReadOnly Property Carousel As Integer
Public Sub New(flightNumber As Integer, from As String, carousel As Integer)
Me.FlightNumber = flightNumber
Me.From = from
Me.Carousel = carousel
End Sub
Public Overloads Function Equals(other As BaggageInfo) As Boolean Implements IEquatable(Of BaggageInfo).Equals
Return FlightNumber = other.FlightNumber AndAlso
From = other.From AndAlso
Carousel = other.Carousel
End Function
Public Overrides Function Equals(obj As Object) As Boolean
If TypeOf obj Is BaggageInfo Then
Return Equals(DirectCast(obj, BaggageInfo))
End If
Return False
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(FlightNumber, From, Carousel)
End Function
Public Shared Operator =(left As BaggageInfo, right As BaggageInfo) As Boolean
Return left.Equals(right)
End Operator
Public Shared Operator <>(left As BaggageInfo, right As BaggageInfo) As Boolean
Return Not left.Equals(right)
End Operator
End Structure
End Namespace
Eine BaggageHandler-Klasse ist für den Empfang von Informationen über ankommende Flüge und die Gepäckausgabebänder verantwortlich. Intern werden zwei Sammlungen verwaltet:
-
_observers: Eine Sammlung von Clients, die aktualisierte Informationen beobachten. -
_flights: Eine Sammlung der Flights und zugewiesenen Gepäckförderbänder
Der Quellcode für die BaggageHandler Klasse wird im folgenden Beispiel gezeigt.
namespace Observables.Example;
public sealed class BaggageHandler : IObservable<BaggageInfo>
{
private readonly Lock _lock = new();
private readonly HashSet<IObserver<BaggageInfo>> _observers = [];
private readonly HashSet<BaggageInfo> _flights = [];
public IDisposable Subscribe(IObserver<BaggageInfo> observer)
{
BaggageInfo[] snapshot;
lock (_lock)
{
// Check whether observer is already registered. If not, add it.
if (!_observers.Add(observer))
{
return new Unsubscriber<BaggageInfo>(_lock, _observers, observer);
}
// Snapshot existing data while holding the lock.
snapshot = [.. _flights];
}
// Provide observer with existing data outside the lock.
foreach (BaggageInfo item in snapshot)
{
observer.OnNext(item);
}
return new Unsubscriber<BaggageInfo>(_lock, _observers, observer);
}
// Called to indicate all baggage is now unloaded.
public void BaggageStatus(int flightNumber) =>
BaggageStatus(flightNumber, string.Empty, 0);
public void BaggageStatus(int flightNumber, string from, int carousel)
{
var info = new BaggageInfo(flightNumber, from, carousel);
IObserver<BaggageInfo>[] snapshot;
// Carousel is assigned, so add new info object to list.
if (carousel > 0)
{
lock (_lock)
{
if (!_flights.Add(info))
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
else if (carousel is 0)
{
// Baggage claim for flight is done.
lock (_lock)
{
if (_flights.RemoveWhere(
flight => flight.FlightNumber == info.FlightNumber) == 0)
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
}
public void LastBaggageClaimed()
{
IObserver<BaggageInfo>[] snapshot;
lock (_lock)
{
snapshot = [.. _observers];
_observers.Clear();
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnCompleted();
}
}
}
Namespace Example
Public NotInheritable Class BaggageHandler
Implements IObservable(Of BaggageInfo)
Private ReadOnly _lock As New Object()
Private ReadOnly _observers As New HashSet(Of IObserver(Of BaggageInfo))()
Private ReadOnly _flights As New HashSet(Of BaggageInfo)()
Public Function Subscribe(observer As IObserver(Of BaggageInfo)) As IDisposable Implements IObservable(Of BaggageInfo).Subscribe
Dim snapshot As BaggageInfo()
SyncLock _lock
' Check whether observer is already registered. If not, add it.
If Not _observers.Add(observer) Then
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End If
' Snapshot existing data while holding the lock.
snapshot = _flights.ToArray()
End SyncLock
' Provide observer with existing data outside the lock.
For Each item As BaggageInfo In snapshot
observer.OnNext(item)
Next
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End Function
' Called to indicate all baggage is now unloaded.
Public Sub BaggageStatus(flightNumber As Integer)
BaggageStatus(flightNumber, String.Empty, 0)
End Sub
Public Sub BaggageStatus(flightNumber As Integer, from As String, carousel As Integer)
Dim info As New BaggageInfo(flightNumber, from, carousel)
Dim snapshot As IObserver(Of BaggageInfo)()
' Carousel is assigned, so add new info object to list.
If carousel > 0 Then
SyncLock _lock
If Not _flights.Add(info) Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
ElseIf carousel = 0 Then
' Baggage claim for flight is done.
SyncLock _lock
If _flights.RemoveWhere(
Function(flight) flight.FlightNumber = info.FlightNumber) = 0 Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
End If
End Sub
Public Sub LastBaggageClaimed()
Dim snapshot As IObserver(Of BaggageInfo)()
SyncLock _lock
snapshot = _observers.ToArray()
_observers.Clear()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnCompleted()
Next
End Sub
End Class
End Namespace
Clients, die aktualisierte Informationen erhalten möchten, rufen die BaggageHandler.Subscribe Methode auf. Wenn der Client zuvor keine Benachrichtigungen abonniert hat, wird der Sammlung ein Verweis auf die IObserver<T> Implementierung des _observers Clients hinzugefügt.
Die überladene BaggageHandler.BaggageStatus-Methode kann aufgerufen werden, um anzugeben, dass Gepäck aus einem Flug entweder entladen oder nicht mehr entladen wird. Im ersten Fall wird der Methode eine Flugnummer, der Flughafen, von dem der Flug gestartet ist, und das Laufband übergeben, auf dem das Gepäck entladen wird. Im zweiten Fall wird der Methode lediglich eine Flugnummer übergeben. Für Gepäck, das entladen wird, überprüft die Methode, ob die BaggageInfo an die Methode übergebenen Informationen in der _flights Sammlung vorhanden sind. Ist dies nicht der Fall, fügt die Methode die Informationen hinzu und ruft die OnNext-Methode aller Beobachter auf. Bei Flügen, deren Gepäck nicht mehr entladen wird, prüft die Methode, ob Informationen zu diesem Flug in der _flights Sammlung gespeichert werden. Wenn dies der Fall ist, ruft die Methode jede Beobachter-OnNext-Methode auf und entfernt das BaggageInfo-Objekt aus der _flights-Sammlung.
Wenn der letzte Flug des Tages gelandet ist und sein Gepäck verarbeitet wurde, wird die BaggageHandler.LastBaggageClaimed Methode aufgerufen. Diese Methode ruft die Methode jedes Beobachters OnCompleted auf, um anzugeben, dass alle Benachrichtigungen abgeschlossen sind, und löscht dann die _observers Auflistung.
Die Methode des Anbieters Subscribe gibt eine IDisposable Implementierung zurück, mit der Beobachter den Empfang von Benachrichtigungen beenden können, bevor die OnCompleted Methode aufgerufen wird. Der Quellcode für diese Unsubscriber Klasse wird im folgenden Beispiel gezeigt. Wenn die Klasse in der BaggageHandler.Subscribe Methode instanziiert wird, wird ein Verweis auf das _lock Objekt, die _observers Auflistung und ein Verweis auf den Beobachter übergeben, der der Auflistung hinzugefügt wird. Diese Verweise werden lokalen Variablen zugewiesen. Wenn die Methode des Dispose Objekts aufgerufen wird, wird der Beobachter aus der _observers Auflistung innerhalb einer Sperre entfernt.
namespace Observables.Example;
internal sealed class Unsubscriber<T> : IDisposable
{
private readonly Lock _lock;
private readonly ISet<IObserver<T>> _observers;
private readonly IObserver<T> _observer;
internal Unsubscriber(
Lock @lock,
ISet<IObserver<T>> observers,
IObserver<T> observer) => (_lock, _observers, _observer) = (@lock, observers, observer);
public void Dispose()
{
lock (_lock)
{
_observers.Remove(_observer);
}
}
}
Namespace Example
Friend NotInheritable Class Unsubscriber(Of T)
Implements IDisposable
Private ReadOnly _lock As Object
Private ReadOnly _observers As ISet(Of IObserver(Of T))
Private ReadOnly _observer As IObserver(Of T)
Friend Sub New(lock As Object, observers As ISet(Of IObserver(Of T)), observer As IObserver(Of T))
_lock = lock
_observers = observers
_observer = observer
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
SyncLock _lock
_observers.Remove(_observer)
End SyncLock
End Sub
End Class
End Namespace
Das folgende Beispiel bietet eine IObserver<T> Implementierung namens ArrivalsMonitor an, die als Basisklasse Informationen zur Gepäckausgabe anzeigt. Die Informationen werden alphabetisch anhand des Namens der Ursprungsstadt angezeigt. Die Methoden ArrivalsMonitor werden als overridable (in Visual Basic) oder virtual (in C#) gekennzeichnet, sodass sie in einer abgeleiteten Klasse überschrieben werden können.
namespace Observables.Example;
public class ArrivalsMonitor : IObserver<BaggageInfo>
{
private readonly string _name;
private readonly Lock _lock = new();
private readonly List<string> _flights = [];
private readonly string _format = "{0,-20} {1,5} {2, 3}";
private IDisposable? _cancellation;
public ArrivalsMonitor(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
_name = name;
}
public virtual void Subscribe(BaggageHandler provider) =>
_cancellation = provider.Subscribe(this);
public virtual void Unsubscribe()
{
Interlocked.Exchange(ref _cancellation, null)?.Dispose();
lock (_lock)
{
_flights.Clear();
}
}
public virtual void OnCompleted()
{
lock (_lock)
{
_flights.Clear();
}
}
// No implementation needed: Method is not called by the BaggageHandler class.
public virtual void OnError(Exception e)
{
// No implementation.
}
// Update information.
public virtual void OnNext(BaggageInfo info)
{
bool updated = false;
lock (_lock)
{
// Flight has unloaded its baggage; remove from the monitor.
if (info.Carousel is 0)
{
string flightNumber = $"{info.FlightNumber,5}";
for (int index = _flights.Count - 1; index >= 0; index--)
{
string flightInfo = _flights[index];
if (flightInfo.Substring(21, 5).Equals(flightNumber))
{
updated = true;
_flights.RemoveAt(index);
}
}
}
else
{
// Add flight if it doesn't exist in the collection.
string flightInfo = string.Format(_format, info.From, info.FlightNumber, info.Carousel);
if (_flights.Contains(flightInfo) is false)
{
_flights.Add(flightInfo);
updated = true;
}
}
if (updated)
{
_flights.Sort();
Console.WriteLine($"Arrivals information from {_name}");
foreach (string flightInfo in _flights)
{
Console.WriteLine(flightInfo);
}
Console.WriteLine();
}
}
}
}
Imports System.Threading
Namespace Example
Public Class ArrivalsMonitor
Implements IObserver(Of BaggageInfo)
Private ReadOnly _name As String
Private ReadOnly _lock As New Object()
Private ReadOnly _flights As New List(Of String)()
Private ReadOnly _format As String = "{0,-20} {1,5} {2, 3}"
Private _cancellation As IDisposable
Public Sub New(name As String)
If String.IsNullOrEmpty(name) Then
Throw New ArgumentException("Value cannot be null or empty.", NameOf(name))
End If
_name = name
End Sub
Public Overridable Sub Subscribe(provider As BaggageHandler)
_cancellation = provider.Subscribe(Me)
End Sub
Public Overridable Sub Unsubscribe()
Dim previous = Interlocked.Exchange(_cancellation, Nothing)
previous?.Dispose()
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
Public Overridable Sub OnCompleted() Implements IObserver(Of BaggageInfo).OnCompleted
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
' No implementation needed: Method is not called by the BaggageHandler class.
Public Overridable Sub OnError([error] As Exception) Implements IObserver(Of BaggageInfo).OnError
' No implementation.
End Sub
' Update information.
Public Overridable Sub OnNext(info As BaggageInfo) Implements IObserver(Of BaggageInfo).OnNext
Dim updated As Boolean = False
SyncLock _lock
' Flight has unloaded its baggage; remove from the monitor.
If info.Carousel = 0 Then
Dim flightNumber As String = String.Format("{0,5}", info.FlightNumber)
For index As Integer = _flights.Count - 1 To 0 Step -1
Dim flightInfo As String = _flights(index)
If flightInfo.Substring(21, 5).Equals(flightNumber) Then
updated = True
_flights.RemoveAt(index)
End If
Next
Else
' Add flight if it doesn't exist in the collection.
Dim flightInfo As String = String.Format(_format, info.From, info.FlightNumber, info.Carousel)
If Not _flights.Contains(flightInfo) Then
_flights.Add(flightInfo)
updated = True
End If
End If
If updated Then
_flights.Sort()
Console.WriteLine($"Arrivals information from {_name}")
For Each flightInfo As String In _flights
Console.WriteLine(flightInfo)
Next
Console.WriteLine()
End If
End SyncLock
End Sub
End Class
End Namespace
Die ArrivalsMonitor Klasse enthält die Subscribe und Unsubscribe Methoden. Die Subscribe Methode ermöglicht es der Klasse, die vom Aufruf an IDisposable zurückgegebene Subscribe Implementierung in einer privaten Variable zu speichern. Die Unsubscribe Methode ermöglicht es der Klasse, das Abonnement von Benachrichtigungen abbestellen zu können, indem die Implementierung des Anbieters Dispose aufgerufen wird.
ArrivalsMonitor bietet außerdem Implementierungen von OnNext, , OnErrorund OnCompleted Methoden. Nur die OnNext Implementierung enthält einen erheblichen Code. Die Methode funktioniert mit einem privaten, sortierten, generischen List<T> Objekt, das Informationen über die Herkunftsflughäfen für ankommende Flüge und die Karussells, auf denen ihr Gepäck verfügbar ist, verwaltet. Wenn die BaggageHandler Klasse eine neue Flugankunft meldet, fügt die OnNext Methodenimplementierung der Liste Informationen zu diesem Flug hinzu. Wenn die BaggageHandler Klasse meldet, dass das Gepäck des Fluges entladen wurde, entfernt die Methode diesen OnNext Flug aus der Liste. Wenn eine Änderung vorgenommen wird, wird die Liste sortiert und in der Konsole angezeigt.
Das folgende Beispiel enthält den Einstiegspunkt der Anwendung, der die BaggageHandler Klasse und zwei Instanzen der ArrivalsMonitor Klasse instanziiert und die BaggageHandler.BaggageStatus Methode verwendet, um Informationen zu eingehenden Flügen hinzuzufügen und zu entfernen. In jedem Fall erhalten die Beobachter Aktualisierungen und zeigen die Informationen zum Gepäckanspruch richtig an.
using Observables.Example;
BaggageHandler provider = new();
ArrivalsMonitor observer1 = new("BaggageClaimMonitor1");
ArrivalsMonitor observer2 = new("SecurityExit");
provider.BaggageStatus(712, "Detroit", 3);
observer1.Subscribe(provider);
provider.BaggageStatus(712, "Kalamazoo", 3);
provider.BaggageStatus(400, "New York-Kennedy", 1);
provider.BaggageStatus(712, "Detroit", 3);
observer2.Subscribe(provider);
provider.BaggageStatus(511, "San Francisco", 2);
provider.BaggageStatus(712);
observer2.Unsubscribe();
provider.BaggageStatus(400);
provider.LastBaggageClaimed();
Imports Observables.Example
Imports System.Threading
Module Program
Sub Main(args As String())
Dim provider As New BaggageHandler()
Dim observer1 As New ArrivalsMonitor("BaggageClaimMonitor1")
Dim observer2 As New ArrivalsMonitor("SecurityExit")
provider.BaggageStatus(712, "Detroit", 3)
observer1.Subscribe(provider)
provider.BaggageStatus(712, "Kalamazoo", 3)
provider.BaggageStatus(400, "New York-Kennedy", 1)
provider.BaggageStatus(712, "Detroit", 3)
observer2.Subscribe(provider)
provider.BaggageStatus(511, "San Francisco", 2)
provider.BaggageStatus(712)
observer2.Unsubscribe()
provider.BaggageStatus(400)
provider.LastBaggageClaimed()
End Sub
End Module