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:
- IObserver<T>.OnNext, die de waarnemer voorziet van nieuwe of actuele informatie.
- IObserver<T>.OnError, waarmee de waarnemer wordt geïnformeerd dat er een fout is opgetreden.
- IObserver<T>.OnCompleted, wat aangeeft dat de provider klaar is met het verzenden van meldingen.
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
Verwante artikelen:
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. |