Beobachter-Entwurfsmuster

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:

  • 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, anstatt IObservable<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