Designmönster för observatör

Designmönstret observer gör det möjligt för en prenumerant att registrera sig med och ta emot meddelanden från en leverantör. Det är lämpligt för alla scenarion som kräver push-baserade meddelanden. Mönstret definierar en provider (även känd som ett ämne eller en observerbar) och noll, en eller flera observatörer. Observatörer registrerar sig hos leverantören, och när ett fördefinierat villkor, händelse eller tillståndsändring inträffar meddelar leverantören automatiskt alla observatörer genom att anropa en delegat. I det här metodanropet kan providern också tillhandahålla aktuell tillståndsinformation till observatörer. I .NET tillämpas mönstret för observatörsdesign genom att implementera de allmänna System.IObservable<T> gränssnitten och System.IObserver<T> gränssnitten. Den generiska typparametern representerar den typ som tillhandahåller meddelandeinformation.

När du ska använda mönstret

Designmönstret för övervakaren är lämpligt för distribuerade push-baserade meddelanden, eftersom det stöder en ren separation mellan två olika komponenter eller programlager, till exempel ett datakälllager (affärslogik) och ett användargränssnittslager (visning). Mönstret kan implementeras när en leverantör använder återanrop för att förse sina klienter med aktuell information.

För att implementera mönstret måste du ange följande information:

  • En leverantör eller ett ämne, vilket är det objekt som skickar meddelanden till observatörer. En provider är en klass eller struktur som implementerar IObservable<T> gränssnittet. Leverantören måste implementera en enda metod, IObservable<T>.Subscribe, som anropas av observatörer som vill ta emot meddelanden från leverantören.

  • En övervakare, som är ett objekt som tar emot meddelanden från en provider. En observatör är en klass eller struktur som implementerar IObserver<T> gränssnittet. Observatören måste implementera tre metoder, som alla anropas av leverantören:

  • En mekanism som gör det möjligt för leverantören att hålla reda på observatörer. Vanligtvis använder providern ett containerobjekt, till exempel ett System.Collections.Generic.List<T> objekt, för att lagra referenser till de IObserver<T> implementeringar som prenumererar på meddelanden. Med hjälp av en lagringscontainer för detta ändamål kan providern hantera noll till ett obegränsat antal observatörer. Ordningen i vilken observatörer tar emot meddelanden definieras inte. leverantören kan använda valfri metod för att fastställa ordningen.

  • En IDisposable implementering som gör det möjligt för leverantören att ta bort observatörer när meddelandet är klart. Observatörer får en referens till implementeringen IDisposable från Subscribe metoden, så att de också kan anropa IDisposable.Dispose metoden för att avbryta prenumerationen innan leverantören har skickat meddelanden.

  • Ett objekt som innehåller de data som providern skickar till sina observatörer. Typen av det här objektet motsvarar den generiska typparametern för gränssnitten IObservable<T> och IObserver<T> . Även om det här objektet kan vara detsamma som implementeringen IObservable<T> är det oftast en separat typ.

Anmärkning

Förutom att implementera mönstret för observatörsdesign kan du vara intresserad av att utforska bibliotek som har skapats med hjälp av gränssnitten IObservable<T> och IObserver<T> . Reaktiva tillägg för .NET (Rx) består till exempel av en uppsättning tilläggsmetoder och LINQ-standardsekvensoperatorer som stöder asynkron programmering.

När du ska överväga alternativ

Gränssnitten IObservable<T>/IObserver<T> passar bra för push-baserade meddelandescenarier, men .NET erbjuder andra mönster som kan passa bättre:

  • Standard .NET-händelser – För enkla meddelandescenarier i ett enda program är events mer idiomatiska och enklare att implementera.
  • IAsyncEnumerable<T> – Använd asynkrona strömmar för asynkrona pull-baserade sekvenser där konsumenten styr takten.
  • System.Threading.Channels – För mönster för producent-konsument med ryggtryck och asynkront stöd använder du System.Threading.Channels.
  • Reactive Extensions (Rx.NET) – För komplex händelsesammansättning, filtrering och transformering använder du paketet System.Reactive i stället för att implementera IObservable<T> direkt.

Den mest framträdande användningen av IObservable<T> i .NET är DiagnosticListener, vilket gör att ramverks- och biblioteksförfattare kan generera strukturerade diagnostikhändelser som konsumenter prenumererar på.

Implementera mönstret

I följande exempel används mönstret för observatörsdesign för att implementera ett informationssystem för bagageanspråk på flygplatsen. En BaggageInfo klass ger information om ankommande flyg och de karuseller där bagage från varje flygning är tillgängligt för upphämtning. Det visas i följande exempel.

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

En BaggageHandler klass ansvarar för att ta emot information om ankommande flyg och bagageutlämningskaruseller. Internt underhåller den två samlingar:

  • _observers: En samling klienter som observerar uppdaterad information.
  • _flights: En samling flygningar och deras tilldelade karuseller.

Källkoden BaggageHandler för klassen visas i följande exempel.

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

Klienter som vill få uppdaterad information anropar BaggageHandler.Subscribe metoden. Om klienten inte har prenumererat på meddelanden tidigare läggs en referens till klientens IObserver<T> implementering i _observers samlingen.

Den överlagrade BaggageHandler.BaggageStatus-metoden kan anropas för att ange att bagage från en flygning antingen lossas eller har slutat lossas. I det första fallet skickas metoden ett flygnummer, flygplatsen från vilken flygresan har sitt ursprung och karusellen där bagaget lossas. I det andra fallet skickas metoden endast ett flygnummer. För bagage som lossas kontrollerar metoden om den BaggageInfo information som skickas till metoden finns i _flights samlingen. Om den inte gör det lägger metoden till informationen och anropar varje observatörs OnNext metod. För flygningar vars bagage inte längre lastas av kontrollerar metoden om information om denna flygning lagras i _flights samlingen. I så fall anropar OnNext metoden varje observatörs BaggageInfo metod och tar bort objektet från _flights samlingen.

När dagens sista flygning har landat och dess bagage har bearbetats, anropas BaggageHandler.LastBaggageClaimed metoden. Den här metoden anropar varje observatörs OnCompleted metod för att ange att alla meddelanden har slutförts och rensar _observers sedan samlingen.

Providerns Subscribe metod returnerar en IDisposable implementering som gör det möjligt för observatörer att sluta ta emot meddelanden innan OnCompleted metoden anropas. Källkoden för den här Unsubscriber klassen visas i följande exempel. När klassen instansieras i BaggageHandler.Subscribe metoden skickas en referens till _lock objektet, _observers samlingen och en referens till den övervakare som läggs till i samlingen. Dessa referenser tilldelas till lokala variabler. När objektets Dispose-metod anropas tas observatören bort från samlingen _observers under låsning.

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

I följande exempel finns en IObserver<T> implementering med namnet ArrivalsMonitor, som är en basklass som visar information om bagageanspråk. Informationen visas alfabetiskt med namnet på den ursprungliga staden. Metoderna ArrivalsMonitor för markeras som overridable (i Visual Basic) eller virtual (i C#), så att de kan åsidosättas i en härledd klass.

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

Klassen ArrivalsMonitor innehåller Subscribe metoderna och Unsubscribe . Med Subscribe metoden kan klassen spara implementeringen IDisposable som returneras av anropet till Subscribe en privat variabel. Metoden Unsubscribe gör det möjligt för klassen att avbryta prenumerationen på meddelanden genom att anropa providerns Dispose implementering. ArrivalsMonitor tillhandahåller även implementeringar av OnNextmetoderna , OnErroroch OnCompleted . Endast implementeringen OnNext innehåller en betydande mängd kod. Metoden fungerar med ett privat, sorterat, generiskt List<T> objekt som lagrar information om ursprungsflygplatserna för ankommande flygningar och karusellerna där deras bagage finns tillgängligt. BaggageHandler När klassen rapporterar en ny flygankomst, lägger metodimplementeringen till information om den flygningen i listan. BaggageHandler Om klassen rapporterar att flygets bagage har avlastats tas OnNext flyget bort från listan. När en ändring görs sorteras listan och visas i konsolen.

Följande exempel innehåller startpunkten för programmet som instansierar BaggageHandler klassen och två instanser av ArrivalsMonitor klassen och använder BaggageHandler.BaggageStatus metoden för att lägga till och ta bort information om ankommande flyg. I varje fall får observatörerna uppdateringar och visar korrekt information om bagageanspråk.

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