Share via


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 ett ombud. 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.

Kommentar

Förutom att implementera designmönstret för övervakare kan du vara intresserad av att utforska bibliotek som skapas 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.

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);
Public Class BaggageInfo
    Private flightNo As Integer
    Private origin As String
    Private location As Integer

    Friend Sub New(ByVal flight As Integer, ByVal from As String, ByVal carousel As Integer)
        Me.flightNo = flight
        Me.origin = from
        Me.location = carousel
    End Sub

    Public ReadOnly Property FlightNumber As Integer
        Get
            Return Me.flightNo
        End Get
    End Property

    Public ReadOnly Property From As String
        Get
            Return Me.origin
        End Get
    End Property

    Public ReadOnly Property Carousel As Integer
        Get
            Return Me.location
        End Get
    End Property
End Class

En BaggageHandler klass ansvarar för att ta emot information om ankommande flyg och bagagefordringar. 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 HashSet<IObserver<BaggageInfo>> _observers = new();
    private readonly HashSet<BaggageInfo> _flights = new();

    public IDisposable Subscribe(IObserver<BaggageInfo> observer)
    {
        // Check whether observer is already registered. If not, add it.
        if (_observers.Add(observer))
        {
            // Provide observer with existing data.
            foreach (BaggageInfo item in _flights)
            {
                observer.OnNext(item);
            }
        }

        return new Unsubscriber<BaggageInfo>(_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);

        // Carousel is assigned, so add new info object to list.
        if (carousel > 0 && _flights.Add(info))
        {
            foreach (IObserver<BaggageInfo> observer in _observers)
            {
                observer.OnNext(info);
            }
        }
        else if (carousel is 0)
        {
            // Baggage claim for flight is done.
            if (_flights.RemoveWhere(
                flight => flight.FlightNumber == info.FlightNumber) > 0)
            {
                foreach (IObserver<BaggageInfo> observer in _observers)
                {
                    observer.OnNext(info);
                }
            }
        }
    }

    public void LastBaggageClaimed()
    {
        foreach (IObserver<BaggageInfo> observer in _observers)
        {
            observer.OnCompleted();
        }

        _observers.Clear();
    }
}
Public Class BaggageHandler : Implements IObservable(Of BaggageInfo)

    Private observers As List(Of IObserver(Of BaggageInfo))
    Private flights As List(Of BaggageInfo)

    Public Sub New()
        observers = New List(Of IObserver(Of BaggageInfo))
        flights = New List(Of BaggageInfo)
    End Sub

    Public Function Subscribe(ByVal observer As IObserver(Of BaggageInfo)) As IDisposable _
                    Implements IObservable(Of BaggageInfo).Subscribe
        ' Check whether observer is already registered. If not, add it
        If Not observers.Contains(observer) Then
            observers.Add(observer)
            ' Provide observer with existing data.
            For Each item In flights
                observer.OnNext(item)
            Next
        End If
        Return New Unsubscriber(Of BaggageInfo)(observers, observer)
    End Function

    ' Called to indicate all baggage is now unloaded.
    Public Sub BaggageStatus(ByVal flightNo As Integer)
        BaggageStatus(flightNo, String.Empty, 0)
    End Sub

    Public Sub BaggageStatus(ByVal flightNo As Integer, ByVal from As String, ByVal carousel As Integer)
        Dim info As New BaggageInfo(flightNo, from, carousel)

        ' Carousel is assigned, so add new info object to list.
        If carousel > 0 And Not flights.Contains(info) Then
            flights.Add(info)
            For Each observer In observers
                observer.OnNext(info)
            Next
        ElseIf carousel = 0 Then
            ' Baggage claim for flight is done
            Dim flightsToRemove As New List(Of BaggageInfo)
            For Each flight In flights
                If info.FlightNumber = flight.FlightNumber Then
                    flightsToRemove.Add(flight)
                    For Each observer In observers
                        observer.OnNext(info)
                    Next
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flights.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        End If
    End Sub

    Public Sub LastBaggageClaimed()
        For Each observer In observers
            observer.OnCompleted()
        Next
        observers.Clear()
    End Sub
End Class

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 inte längre 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 BaggageInfo metoden varje observatörs OnNext metod och tar bort objektet från _flights samlingen.

När dagens sista flygning har landat och dess bagage har bearbetats BaggageHandler.LastBaggageClaimed anropas 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(Of BaggageInfo) klassen visas i följande exempel. När klassen instansieras i -metoden skickas en referens till _observers samlingen och en referens till den observatör som läggs till i BaggageHandler.Subscribe samlingen. Dessa referenser tilldelas till lokala variabler. När objektets -metod anropas Dispose kontrollerar den om övervakaren fortfarande finns i _observers samlingen och, om den gör det, tar bort övervakaren.

namespace Observables.Example;

internal sealed class Unsubscriber<BaggageInfo> : IDisposable
{
    private readonly ISet<IObserver<BaggageInfo>> _observers;
    private readonly IObserver<BaggageInfo> _observer;

    internal Unsubscriber(
        ISet<IObserver<BaggageInfo>> observers,
        IObserver<BaggageInfo> observer) => (_observers, _observer) = (observers, observer);

    public void Dispose() => _observers.Remove(_observer);
}
Friend Class Unsubscriber(Of BaggageInfo) : Implements IDisposable
    Private _observers As List(Of IObserver(Of BaggageInfo))
    Private _observer As IObserver(Of BaggageInfo)

    Friend Sub New(ByVal observers As List(Of IObserver(Of BaggageInfo)), ByVal observer As IObserver(Of BaggageInfo))
        Me._observers = observers
        Me._observer = observer
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        If _observers.Contains(_observer) Then
            _observers.Remove(_observer)
        End If
    End Sub
End Class

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 List<string> _flights = new();
    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()
    {
        _cancellation?.Dispose();
        _flights.Clear();
    }

    public virtual void OnCompleted() => _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;

        // Flight has unloaded its baggage; remove from the monitor.
        if (info.Carousel is 0)
        {
            string flightNumber = string.Format("{0,5}", info.FlightNumber);
            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();
        }
    }
}
Public Class ArrivalsMonitor : Implements IObserver(Of BaggageInfo)
    Private name As String
    Private flightInfos As New List(Of String)
    Private cancellation As IDisposable
    Private fmt As String = "{0,-20} {1,5}  {2, 3}"

    Public Sub New(ByVal name As String)
        If String.IsNullOrEmpty(name) Then Throw New ArgumentNullException("The observer must be assigned a name.")

        Me.name = name
    End Sub

    Public Overridable Sub Subscribe(ByVal provider As BaggageHandler)
        cancellation = provider.Subscribe(Me)
    End Sub

    Public Overridable Sub Unsubscribe()
        cancellation.Dispose()
        flightInfos.Clear()
    End Sub

    Public Overridable Sub OnCompleted() Implements System.IObserver(Of BaggageInfo).OnCompleted
        flightInfos.Clear()
    End Sub

    ' No implementation needed: Method is not called by the BaggageHandler class.
    Public Overridable Sub OnError(ByVal e As System.Exception) Implements System.IObserver(Of BaggageInfo).OnError
        ' No implementation.
    End Sub

    ' Update information.
    Public Overridable Sub OnNext(ByVal info As BaggageInfo) Implements System.IObserver(Of BaggageInfo).OnNext
        Dim updated As Boolean = False

        ' Flight has unloaded its baggage; remove from the monitor.
        If info.Carousel = 0 Then
            Dim flightsToRemove As New List(Of String)
            Dim flightNo As String = String.Format("{0,5}", info.FlightNumber)
            For Each flightInfo In flightInfos
                If flightInfo.Substring(21, 5).Equals(flightNo) Then
                    flightsToRemove.Add(flightInfo)
                    updated = True
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flightInfos.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        Else
            ' Add flight if it does not exist in the collection.
            Dim flightInfo As String = String.Format(fmt, info.From, info.FlightNumber, info.Carousel)
            If Not flightInfos.Contains(flightInfo) Then
                flightInfos.Add(flightInfo)
                updated = True
            End If
        End If
        If updated Then
            flightInfos.Sort()
            Console.WriteLine("Arrivals information from {0}", Me.name)
            For Each flightInfo In flightInfos
                Console.WriteLine(flightInfo)
            Next
            Console.WriteLine()
        End If
    End Sub
End Class

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 Om klassen rapporterar en ny flyg ankomst lägger OnNext metodimplementeringen till information om den flygningen i listan. BaggageHandler Om klassen rapporterar att flygets bagage har lossats tas OnNext flygresan 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();

// Sample output:
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from BaggageClaimMonitor1
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from SecurityExit
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from BaggageClaimMonitor1
//   San Francisco          511    2
Module Example
    Public Sub Main()
        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
' The example displays the following output:
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from BaggageClaimMonitor1
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from SecurityExit
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from BaggageClaimMonitor1
'      San Francisco          511    2
Title Description
Metodtips för observatörsdesignmönster Beskriver metodtips som ska användas när du utvecklar program som implementerar designmönstret för övervakare.
Gör så här: Implementera en provider Tillhandahåller en stegvis implementering av en provider för ett temperaturövervakningsprogram.
Gör så här: Implementera en observatör Tillhandahåller en stegvis implementering av en övervakare för ett temperaturövervakningsprogram.