Delen via


Ontwerppatroon waarnemer

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 nul verwerken 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.

Notitie

Naast het implementeren van het ontwerppatroon waarnemers, 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.

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);
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

Een BaggageHandler klasse is verantwoordelijk voor het ontvangen van informatie over aankomende vluchten en bagageclaims. 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 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

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 IObserver<T> de client toegevoegd aan de _observers verzameling.

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 wordt de methode doorgegeven aan een vluchtnummer, de luchthaven van waaruit de vlucht afkomstig is en de carrousel waar bagage wordt ontladen. In het tweede geval wordt de methode alleen een vluchtnummer 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 dit niet het probleem 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 van elke waarnemer OnNext de methode aan en verwijdert u 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 methode van OnCompleted elke waarnemer aangeroepen om aan te geven dat alle meldingen zijn voltooid en 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(Of BaggageInfo) klasse wordt weergegeven in het volgende voorbeeld. Wanneer de klasse wordt geïnstantieerd in de BaggageHandler.Subscribe methode, wordt er een verwijzing doorgegeven aan de _observers verzameling en een verwijzing naar de waarnemer die wordt toegevoegd aan de verzameling. Deze verwijzingen worden toegewezen aan lokale variabelen. Wanneer de methode van Dispose het object wordt aangeroepen, wordt gecontroleerd of de waarnemer nog steeds bestaat in de _observers verzameling en, als dit het geval is, de waarnemer verwijdert.

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

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 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

De ArrivalsMonitor klasse bevat de Subscribe en Unsubscribe methoden. Met Subscribe de methode kan de klasse de IDisposable implementatie opslaan die door de aanroep naar Subscribe een privévariabele wordt geretourneerd. 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 verwijderd, OnNext wordt die vlucht uit de lijst verwijderd. 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();

// 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 Beschrijving
Best practices voor waarnemersontwerppatroon Beschrijft aanbevolen procedures voor het ontwikkelen van toepassingen die het ontwerppatroon van de waarnemer implementeren.
Procedure: Een provider implementeren Biedt een stapsgewijze implementatie van een provider voor een toepassing voor temperatuurbewaking.
Procedure: Een waarnemer implementeren Biedt een stapsgewijze implementatie van een waarnemer voor een toepassing voor temperatuurbewaking.