Waarnemer ontwerppatroon

Met het ontwerppatroon van de waarnemer kan een abonnee zich registreren bij en meldingen ontvangen van een provider. Het is geschikt voor elk scenario waarvoor pushmeldingen zijn vereist. Het patroon definieert een provider (ook wel een onderwerp of waarneembaar genoemd) en nul, een of meer waarnemers. Waarnemers registreren zich bij de provider en wanneer een vooraf gedefinieerde voorwaarde, gebeurtenis of statuswijziging plaatsvindt, meldt de provider automatisch alle waarnemers aan door een gemachtigde aan te roepen. In deze methode-aanroep kan de provider ook actuele statusinformatie verstrekken aan waarnemers. In .NET wordt het ontwerppatroon van de waarnemer toegepast door de algemene System.IObservable<T> en System.IObserver<T> interfaces te implementeren. De algemene typeparameter vertegenwoordigt het type dat meldingsgegevens levert.

Wanneer moet u het patroon toepassen

Het ontwerppatroon van de waarnemer is geschikt voor gedistribueerde pushmeldingen, omdat het ondersteuning biedt voor een schone scheiding tussen twee verschillende onderdelen of toepassingslagen, zoals een gegevensbronlaag (bedrijfslogica) en een gebruikersinterfacelaag (weergavelaag). Het patroon kan worden geïmplementeerd wanneer een provider callbacks gebruikt om klanten actuele informatie te verstrekken.

Voor het implementeren van het patroon moet u de volgende details opgeven:

  • Een provider of onderwerp, het object dat meldingen verzendt naar waarnemers. Een provider is een klasse of structuur waarmee de IObservable<T> interface wordt geïmplementeerd. De provider moet één methode implementeren, IObservable<T>.Subscribedie wordt aangeroepen door waarnemers die meldingen van de provider willen ontvangen.

  • Een waarnemer, een object dat meldingen van een provider ontvangt. Een waarnemer is een klasse of structuur waarmee de IObserver<T> interface wordt geïmplementeerd. De waarnemer moet drie methoden implementeren, die allemaal worden aangeroepen door de provider:

  • Een mechanisme waarmee de provider waarnemers kan bijhouden. Normaal gesproken gebruikt de provider een containerobject, zoals een System.Collections.Generic.List<T> object, om verwijzingen te bewaren naar de IObserver<T> implementaties die zijn geabonneerd op meldingen. Met behulp van een opslagcontainer voor dit doel kan de provider omgaan met nul tot een onbeperkt aantal waarnemers. De volgorde waarin waarnemers meldingen ontvangen, is niet gedefinieerd; de provider mag elke methode gebruiken om de bestelling te bepalen.

  • Een IDisposable implementatie waarmee de provider waarnemers kan verwijderen wanneer de melding is voltooid. Waarnemers ontvangen een verwijzing naar de implementatie van de IDisposableSubscribe methode, zodat ze ook de IDisposable.Dispose methode kunnen aanroepen om zich af te melden voordat de provider klaar is met het verzenden van meldingen.

  • Een object dat de gegevens bevat die de provider naar de waarnemers verzendt. Het type van dit object komt overeen met de algemene typeparameter van de IObservable<T> en IObserver<T> interfaces. Hoewel dit object hetzelfde kan zijn als de IObservable<T> implementatie, is het meestal een afzonderlijk type.

Opmerking

Naast het implementeren van het ontwerppatroon van de waarnemer, bent u mogelijk geïnteresseerd in het verkennen van bibliotheken die zijn gebouwd met behulp van de IObservable<T> en IObserver<T> interfaces. Reactive Extensions voor .NET (Rx) bestaan bijvoorbeeld uit een set extensiemethoden en LINQ-standaardreeksoperators ter ondersteuning van asynchrone programmering.

Wanneer u alternatieven moet overwegen

De interfaces IObservable<T>/IObserver<T> zijn geschikt voor pushgebaseerde meldingsscenario's, maar .NET biedt andere patronen die mogelijk beter passen:

  • Standaard-.NET-events — Voor eenvoudige meldingsscenario's binnen één applicatie zijn events gebruikelijker en eenvoudiger te implementeren.
  • IAsyncEnumerable<T> — Voor asynchrone pull-reeksen waarbij de consument het tempo beheert, gebruikt u asynchrone streams.
  • System.Threading.Channels — Gebruik System.Threading.Channels voor producent-consumentpatronen met backpressure en asynchrone ondersteuning.
  • Reactive Extensions (Rx.NET) — Gebruik voor complexe gebeurtenissamenstelling, filtering en transformatie het pakket System.Reactive in plaats van IObservable<T> direct te implementeren.

Het meest prominente gebruik van IObservable<T> in .NET is DiagnosticListener, waarmee framework- en bibliotheekauteurs gestructureerde diagnostische gebeurtenissen kunnen verzenden waarop consumenten zich abonneren.

Het patroon implementeren

In het volgende voorbeeld wordt het ontwerppatroon van de waarnemer gebruikt om een informatiesysteem voor bagageclaims voor luchthavenbagage te implementeren. Een BaggageInfo klasse biedt informatie over aankomende vluchten en de carrousels waar bagage van elke vlucht beschikbaar is voor ophalen. Deze wordt weergegeven in het volgende voorbeeld.

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

Een BaggageHandler klasse is verantwoordelijk voor het ontvangen van informatie over aankomende vluchten en bagagecarrousels. Intern onderhoudt het twee verzamelingen:

  • _observers: Een verzameling clients die bijgewerkte informatie observeren.
  • _flights: Een verzameling vluchten en hun toegewezen carrousels.

De broncode voor de BaggageHandler klasse wordt weergegeven in het volgende voorbeeld.

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 bijgewerkte informatie willen ontvangen, roepen de BaggageHandler.Subscribe methode aan. Als de client zich nog niet eerder heeft geabonneerd op meldingen, wordt er een verwijzing naar de implementatie van de client IObserver<T> toegevoegd aan de _observers collectie.

De overbelaste BaggageHandler.BaggageStatus methode kan worden aangeroepen om aan te geven dat bagage van een vlucht wordt verwijderd of niet meer wordt verwijderd. In het eerste geval krijgt de methode een vluchtnummer, de luchthaven waar de vlucht vertrekt, en de bagageband waar de bagage wordt gelost. In het tweede geval wordt er alleen een vluchtnummer aan de methode doorgegeven. Voor bagage die wordt uitgepakt, controleert de methode of de BaggageInfo informatie die aan de methode is doorgegeven, aanwezig is in de _flights verzameling. Als dat niet het geval is, voegt de methode de informatie toe en roept de waarnemersmethode OnNext aan. Voor vluchten waarvan de bagage niet meer wordt geladen, controleert de methode of informatie over die vlucht wordt opgeslagen in de _flights verzameling. Als dat zo is, roept de methode elke waarnemer hun OnNext-methode aan en verwijdert het BaggageInfo-object uit de _flights-verzameling.

Wanneer de laatste vlucht van de dag is geland en de bagage is verwerkt, wordt de BaggageHandler.LastBaggageClaimed methode aangeroepen. Met deze methode wordt de OnCompleted-methode van elke waarnemer aangeroepen om aan te geven dat alle meldingen zijn voltooid. Vervolgens wordt de _observers-verzameling gewist.

De methode van Subscribe de provider retourneert een IDisposable implementatie waarmee waarnemers geen meldingen meer kunnen ontvangen voordat de OnCompleted methode wordt aangeroepen. De broncode voor deze Unsubscriber klasse wordt weergegeven in het volgende voorbeeld. Wanneer de klasse wordt geïnstantieerd in de BaggageHandler.Subscribe methode, wordt er een verwijzing doorgegeven naar het _lock object, de _observers verzameling en een verwijzing naar de waarnemer die wordt toegevoegd aan de verzameling. Deze verwijzingen worden toegewezen aan lokale variabelen. Wanneer de Dispose-methode van het object wordt aangeroepen, wordt de observer uit de _observers-verzameling verwijderd binnen een vergrendelde sectie.

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

In het volgende voorbeeld ziet u een implementatie met de IObserver<T> naam ArrivalsMonitor, een basisklasse die informatie over bagageclaims weergeeft. De informatie wordt alfabetisch weergegeven op basis van de naam van de oorspronkelijke stad. De methoden ArrivalsMonitor worden gemarkeerd als overridable (in Visual Basic) of virtual (in C#), zodat ze kunnen worden overschreven in een afgeleide klasse.

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

De ArrivalsMonitor klasse bevat de Subscribe en Unsubscribe methoden. Met de Subscribe-methode kan de klasse de IDisposable-implementatie, die door de aanroep naar Subscribe wordt geretourneerd, in een privévariabele opslaan. Met Unsubscribe de methode kan de klasse zich afmelden voor meldingen door de implementatie van Dispose de provider aan te roepen. ArrivalsMonitor biedt ook implementaties van de OnNext, OnErroren OnCompleted methoden. Alleen de OnNext implementatie bevat een aanzienlijke hoeveelheid code. De methode werkt met een privé, gesorteerd, algemeen List<T> object dat informatie onderhoudt over de luchthavens van herkomst voor het aankomen van vluchten en de carrousels waarop hun bagage beschikbaar is. Als de BaggageHandler klasse een nieuwe vlucht aankomst rapporteert, voegt de implementatie van de OnNext methode informatie over die vlucht toe aan de lijst. Als de BaggageHandler klasse meldt dat de bagage van de vlucht is uitgeladen, verwijdert de OnNext methode die vlucht uit de lijst. Wanneer er een wijziging wordt aangebracht, wordt de lijst gesorteerd en weergegeven in de console.

Het volgende voorbeeld bevat het invoerpunt van de toepassing waarmee de BaggageHandler klasse en twee exemplaren van de ArrivalsMonitor klasse worden geïnstitueert en de BaggageHandler.BaggageStatus methode wordt gebruikt om informatie over het binnenkomend vluchten toe te voegen en te verwijderen. In elk geval ontvangen de waarnemers updates en geven ze informatie over bagageclaims correct weer.

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