Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of mappen te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen om mappen te wijzigen.
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 omgaan met nul tot een onbeperkt aantal waarnemers. De volgorde waarin waarnemers meldingen ontvangen, is niet gedefinieerd; de provider mag elke methode gebruiken om de bestelling te bepalen.
Een IDisposable implementatie waarmee de provider waarnemers kan verwijderen wanneer de melding is voltooid. Waarnemers ontvangen een verwijzing naar de implementatie van de IDisposableSubscribe methode, zodat ze ook de IDisposable.Dispose methode kunnen aanroepen om zich af te melden voordat de provider klaar is met het verzenden van meldingen.
Een object dat de gegevens bevat die de provider naar de waarnemers verzendt. Het type van dit object komt overeen met de algemene typeparameter van de IObservable<T> en IObserver<T> interfaces. Hoewel dit object hetzelfde kan zijn als de IObservable<T> implementatie, is het meestal een afzonderlijk type.
Opmerking
Naast het implementeren van het ontwerppatroon van de waarnemer, bent u mogelijk geïnteresseerd in het verkennen van bibliotheken die zijn gebouwd met behulp van de IObservable<T> en IObserver<T> interfaces. Reactive Extensions voor .NET (Rx) bestaan bijvoorbeeld uit een set extensiemethoden en LINQ-standaardreeksoperators ter ondersteuning van asynchrone programmering.
Wanneer u alternatieven moet overwegen
De interfaces IObservable<T>/IObserver<T> zijn geschikt voor pushgebaseerde meldingsscenario's, maar .NET biedt andere patronen die mogelijk beter passen:
- Standaard-.NET-events — Voor eenvoudige meldingsscenario's binnen één applicatie zijn events gebruikelijker en eenvoudiger te implementeren.
-
IAsyncEnumerable<T>— Voor asynchrone pull-reeksen waarbij de consument het tempo beheert, gebruikt u asynchrone streams. -
System.Threading.Channels— Gebruik System.Threading.Channels voor producent-consumentpatronen met backpressure en asynchrone ondersteuning. -
Reactive Extensions (Rx.NET) — Gebruik voor complexe gebeurtenissamenstelling, filtering en transformatie het pakket
System.Reactivein plaats vanIObservable<T>direct te implementeren.
Het meest prominente gebruik van IObservable<T> in .NET is DiagnosticListener, waarmee framework- en bibliotheekauteurs gestructureerde diagnostische gebeurtenissen kunnen verzenden waarop consumenten zich abonneren.
Het patroon implementeren
In het volgende voorbeeld wordt het ontwerppatroon van de waarnemer gebruikt om een informatiesysteem voor bagageclaims voor luchthavenbagage te implementeren. Een BaggageInfo klasse biedt informatie over aankomende vluchten en de carrousels waar bagage van elke vlucht beschikbaar is voor ophalen. Deze wordt weergegeven in het volgende voorbeeld.
namespace Observables.Example;
public readonly record struct BaggageInfo(
int FlightNumber,
string From,
int Carousel);
Namespace Example
Public Structure BaggageInfo
Implements IEquatable(Of BaggageInfo)
Public ReadOnly Property FlightNumber As Integer
Public ReadOnly Property From As String
Public ReadOnly Property Carousel As Integer
Public Sub New(flightNumber As Integer, from As String, carousel As Integer)
Me.FlightNumber = flightNumber
Me.From = from
Me.Carousel = carousel
End Sub
Public Overloads Function Equals(other As BaggageInfo) As Boolean Implements IEquatable(Of BaggageInfo).Equals
Return FlightNumber = other.FlightNumber AndAlso
From = other.From AndAlso
Carousel = other.Carousel
End Function
Public Overrides Function Equals(obj As Object) As Boolean
If TypeOf obj Is BaggageInfo Then
Return Equals(DirectCast(obj, BaggageInfo))
End If
Return False
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(FlightNumber, From, Carousel)
End Function
Public Shared Operator =(left As BaggageInfo, right As BaggageInfo) As Boolean
Return left.Equals(right)
End Operator
Public Shared Operator <>(left As BaggageInfo, right As BaggageInfo) As Boolean
Return Not left.Equals(right)
End Operator
End Structure
End Namespace
Een BaggageHandler klasse is verantwoordelijk voor het ontvangen van informatie over aankomende vluchten en bagagecarrousels. Intern onderhoudt het twee verzamelingen:
-
_observers: Een verzameling clients die bijgewerkte informatie observeren. -
_flights: Een verzameling vluchten en hun toegewezen carrousels.
De broncode voor de BaggageHandler klasse wordt weergegeven in het volgende voorbeeld.
namespace Observables.Example;
public sealed class BaggageHandler : IObservable<BaggageInfo>
{
private readonly Lock _lock = new();
private readonly HashSet<IObserver<BaggageInfo>> _observers = [];
private readonly HashSet<BaggageInfo> _flights = [];
public IDisposable Subscribe(IObserver<BaggageInfo> observer)
{
BaggageInfo[] snapshot;
lock (_lock)
{
// Check whether observer is already registered. If not, add it.
if (!_observers.Add(observer))
{
return new Unsubscriber<BaggageInfo>(_lock, _observers, observer);
}
// Snapshot existing data while holding the lock.
snapshot = [.. _flights];
}
// Provide observer with existing data outside the lock.
foreach (BaggageInfo item in snapshot)
{
observer.OnNext(item);
}
return new Unsubscriber<BaggageInfo>(_lock, _observers, observer);
}
// Called to indicate all baggage is now unloaded.
public void BaggageStatus(int flightNumber) =>
BaggageStatus(flightNumber, string.Empty, 0);
public void BaggageStatus(int flightNumber, string from, int carousel)
{
var info = new BaggageInfo(flightNumber, from, carousel);
IObserver<BaggageInfo>[] snapshot;
// Carousel is assigned, so add new info object to list.
if (carousel > 0)
{
lock (_lock)
{
if (!_flights.Add(info))
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
else if (carousel is 0)
{
// Baggage claim for flight is done.
lock (_lock)
{
if (_flights.RemoveWhere(
flight => flight.FlightNumber == info.FlightNumber) == 0)
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
}
public void LastBaggageClaimed()
{
IObserver<BaggageInfo>[] snapshot;
lock (_lock)
{
snapshot = [.. _observers];
_observers.Clear();
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnCompleted();
}
}
}
Namespace Example
Public NotInheritable Class BaggageHandler
Implements IObservable(Of BaggageInfo)
Private ReadOnly _lock As New Object()
Private ReadOnly _observers As New HashSet(Of IObserver(Of BaggageInfo))()
Private ReadOnly _flights As New HashSet(Of BaggageInfo)()
Public Function Subscribe(observer As IObserver(Of BaggageInfo)) As IDisposable Implements IObservable(Of BaggageInfo).Subscribe
Dim snapshot As BaggageInfo()
SyncLock _lock
' Check whether observer is already registered. If not, add it.
If Not _observers.Add(observer) Then
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End If
' Snapshot existing data while holding the lock.
snapshot = _flights.ToArray()
End SyncLock
' Provide observer with existing data outside the lock.
For Each item As BaggageInfo In snapshot
observer.OnNext(item)
Next
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End Function
' Called to indicate all baggage is now unloaded.
Public Sub BaggageStatus(flightNumber As Integer)
BaggageStatus(flightNumber, String.Empty, 0)
End Sub
Public Sub BaggageStatus(flightNumber As Integer, from As String, carousel As Integer)
Dim info As New BaggageInfo(flightNumber, from, carousel)
Dim snapshot As IObserver(Of BaggageInfo)()
' Carousel is assigned, so add new info object to list.
If carousel > 0 Then
SyncLock _lock
If Not _flights.Add(info) Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
ElseIf carousel = 0 Then
' Baggage claim for flight is done.
SyncLock _lock
If _flights.RemoveWhere(
Function(flight) flight.FlightNumber = info.FlightNumber) = 0 Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
End If
End Sub
Public Sub LastBaggageClaimed()
Dim snapshot As IObserver(Of BaggageInfo)()
SyncLock _lock
snapshot = _observers.ToArray()
_observers.Clear()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnCompleted()
Next
End Sub
End Class
End Namespace
Clients die bijgewerkte informatie willen ontvangen, roepen de BaggageHandler.Subscribe methode aan. Als de client zich nog niet eerder heeft geabonneerd op meldingen, wordt er een verwijzing naar de implementatie van de client IObserver<T> toegevoegd aan de _observers collectie.
De overbelaste BaggageHandler.BaggageStatus methode kan worden aangeroepen om aan te geven dat bagage van een vlucht wordt verwijderd of niet meer wordt verwijderd. In het eerste geval krijgt de methode een vluchtnummer, de luchthaven waar de vlucht vertrekt, en de bagageband waar de bagage wordt gelost. In het tweede geval wordt er alleen een vluchtnummer aan de methode doorgegeven. Voor bagage die wordt uitgepakt, controleert de methode of de BaggageInfo informatie die aan de methode is doorgegeven, aanwezig is in de _flights verzameling. Als dat niet het geval is, voegt de methode de informatie toe en roept de waarnemersmethode OnNext aan. Voor vluchten waarvan de bagage niet meer wordt geladen, controleert de methode of informatie over die vlucht wordt opgeslagen in de _flights verzameling. Als dat zo is, roept de methode elke waarnemer hun OnNext-methode aan en verwijdert het BaggageInfo-object uit de _flights-verzameling.
Wanneer de laatste vlucht van de dag is geland en de bagage is verwerkt, wordt de BaggageHandler.LastBaggageClaimed methode aangeroepen. Met deze methode wordt de OnCompleted-methode van elke waarnemer aangeroepen om aan te geven dat alle meldingen zijn voltooid. Vervolgens wordt de _observers-verzameling gewist.
De methode van Subscribe de provider retourneert een IDisposable implementatie waarmee waarnemers geen meldingen meer kunnen ontvangen voordat de OnCompleted methode wordt aangeroepen. De broncode voor deze Unsubscriber klasse wordt weergegeven in het volgende voorbeeld. Wanneer de klasse wordt geïnstantieerd in de BaggageHandler.Subscribe methode, wordt er een verwijzing doorgegeven naar het _lock object, de _observers verzameling en een verwijzing naar de waarnemer die wordt toegevoegd aan de verzameling. Deze verwijzingen worden toegewezen aan lokale variabelen. Wanneer de Dispose-methode van het object wordt aangeroepen, wordt de observer uit de _observers-verzameling verwijderd binnen een vergrendelde sectie.
namespace Observables.Example;
internal sealed class Unsubscriber<T> : IDisposable
{
private readonly Lock _lock;
private readonly ISet<IObserver<T>> _observers;
private readonly IObserver<T> _observer;
internal Unsubscriber(
Lock @lock,
ISet<IObserver<T>> observers,
IObserver<T> observer) => (_lock, _observers, _observer) = (@lock, observers, observer);
public void Dispose()
{
lock (_lock)
{
_observers.Remove(_observer);
}
}
}
Namespace Example
Friend NotInheritable Class Unsubscriber(Of T)
Implements IDisposable
Private ReadOnly _lock As Object
Private ReadOnly _observers As ISet(Of IObserver(Of T))
Private ReadOnly _observer As IObserver(Of T)
Friend Sub New(lock As Object, observers As ISet(Of IObserver(Of T)), observer As IObserver(Of T))
_lock = lock
_observers = observers
_observer = observer
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
SyncLock _lock
_observers.Remove(_observer)
End SyncLock
End Sub
End Class
End Namespace
In het volgende voorbeeld ziet u een implementatie met de IObserver<T> naam ArrivalsMonitor, een basisklasse die informatie over bagageclaims weergeeft. De informatie wordt alfabetisch weergegeven op basis van de naam van de oorspronkelijke stad. De methoden ArrivalsMonitor worden gemarkeerd als overridable (in Visual Basic) of virtual (in C#), zodat ze kunnen worden overschreven in een afgeleide klasse.
namespace Observables.Example;
public class ArrivalsMonitor : IObserver<BaggageInfo>
{
private readonly string _name;
private readonly Lock _lock = new();
private readonly List<string> _flights = [];
private readonly string _format = "{0,-20} {1,5} {2, 3}";
private IDisposable? _cancellation;
public ArrivalsMonitor(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
_name = name;
}
public virtual void Subscribe(BaggageHandler provider) =>
_cancellation = provider.Subscribe(this);
public virtual void Unsubscribe()
{
Interlocked.Exchange(ref _cancellation, null)?.Dispose();
lock (_lock)
{
_flights.Clear();
}
}
public virtual void OnCompleted()
{
lock (_lock)
{
_flights.Clear();
}
}
// No implementation needed: Method is not called by the BaggageHandler class.
public virtual void OnError(Exception e)
{
// No implementation.
}
// Update information.
public virtual void OnNext(BaggageInfo info)
{
bool updated = false;
lock (_lock)
{
// Flight has unloaded its baggage; remove from the monitor.
if (info.Carousel is 0)
{
string flightNumber = $"{info.FlightNumber,5}";
for (int index = _flights.Count - 1; index >= 0; index--)
{
string flightInfo = _flights[index];
if (flightInfo.Substring(21, 5).Equals(flightNumber))
{
updated = true;
_flights.RemoveAt(index);
}
}
}
else
{
// Add flight if it doesn't exist in the collection.
string flightInfo = string.Format(_format, info.From, info.FlightNumber, info.Carousel);
if (_flights.Contains(flightInfo) is false)
{
_flights.Add(flightInfo);
updated = true;
}
}
if (updated)
{
_flights.Sort();
Console.WriteLine($"Arrivals information from {_name}");
foreach (string flightInfo in _flights)
{
Console.WriteLine(flightInfo);
}
Console.WriteLine();
}
}
}
}
Imports System.Threading
Namespace Example
Public Class ArrivalsMonitor
Implements IObserver(Of BaggageInfo)
Private ReadOnly _name As String
Private ReadOnly _lock As New Object()
Private ReadOnly _flights As New List(Of String)()
Private ReadOnly _format As String = "{0,-20} {1,5} {2, 3}"
Private _cancellation As IDisposable
Public Sub New(name As String)
If String.IsNullOrEmpty(name) Then
Throw New ArgumentException("Value cannot be null or empty.", NameOf(name))
End If
_name = name
End Sub
Public Overridable Sub Subscribe(provider As BaggageHandler)
_cancellation = provider.Subscribe(Me)
End Sub
Public Overridable Sub Unsubscribe()
Dim previous = Interlocked.Exchange(_cancellation, Nothing)
previous?.Dispose()
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
Public Overridable Sub OnCompleted() Implements IObserver(Of BaggageInfo).OnCompleted
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
' No implementation needed: Method is not called by the BaggageHandler class.
Public Overridable Sub OnError([error] As Exception) Implements IObserver(Of BaggageInfo).OnError
' No implementation.
End Sub
' Update information.
Public Overridable Sub OnNext(info As BaggageInfo) Implements IObserver(Of BaggageInfo).OnNext
Dim updated As Boolean = False
SyncLock _lock
' Flight has unloaded its baggage; remove from the monitor.
If info.Carousel = 0 Then
Dim flightNumber As String = String.Format("{0,5}", info.FlightNumber)
For index As Integer = _flights.Count - 1 To 0 Step -1
Dim flightInfo As String = _flights(index)
If flightInfo.Substring(21, 5).Equals(flightNumber) Then
updated = True
_flights.RemoveAt(index)
End If
Next
Else
' Add flight if it doesn't exist in the collection.
Dim flightInfo As String = String.Format(_format, info.From, info.FlightNumber, info.Carousel)
If Not _flights.Contains(flightInfo) Then
_flights.Add(flightInfo)
updated = True
End If
End If
If updated Then
_flights.Sort()
Console.WriteLine($"Arrivals information from {_name}")
For Each flightInfo As String In _flights
Console.WriteLine(flightInfo)
Next
Console.WriteLine()
End If
End SyncLock
End Sub
End Class
End Namespace
De ArrivalsMonitor klasse bevat de Subscribe en Unsubscribe methoden. Met de Subscribe-methode kan de klasse de IDisposable-implementatie, die door de aanroep naar Subscribe wordt geretourneerd, in een privévariabele opslaan. Met Unsubscribe de methode kan de klasse zich afmelden voor meldingen door de implementatie van Dispose de provider aan te roepen.
ArrivalsMonitor biedt ook implementaties van de OnNext, OnErroren OnCompleted methoden. Alleen de OnNext implementatie bevat een aanzienlijke hoeveelheid code. De methode werkt met een privé, gesorteerd, algemeen List<T> object dat informatie onderhoudt over de luchthavens van herkomst voor het aankomen van vluchten en de carrousels waarop hun bagage beschikbaar is. Als de BaggageHandler klasse een nieuwe vlucht aankomst rapporteert, voegt de implementatie van de OnNext methode informatie over die vlucht toe aan de lijst. Als de BaggageHandler klasse meldt dat de bagage van de vlucht is uitgeladen, verwijdert de OnNext methode die vlucht uit de lijst. Wanneer er een wijziging wordt aangebracht, wordt de lijst gesorteerd en weergegeven in de console.
Het volgende voorbeeld bevat het invoerpunt van de toepassing waarmee de BaggageHandler klasse en twee exemplaren van de ArrivalsMonitor klasse worden geïnstitueert en de BaggageHandler.BaggageStatus methode wordt gebruikt om informatie over het binnenkomend vluchten toe te voegen en te verwijderen. In elk geval ontvangen de waarnemers updates en geven ze informatie over bagageclaims correct weer.
using Observables.Example;
BaggageHandler provider = new();
ArrivalsMonitor observer1 = new("BaggageClaimMonitor1");
ArrivalsMonitor observer2 = new("SecurityExit");
provider.BaggageStatus(712, "Detroit", 3);
observer1.Subscribe(provider);
provider.BaggageStatus(712, "Kalamazoo", 3);
provider.BaggageStatus(400, "New York-Kennedy", 1);
provider.BaggageStatus(712, "Detroit", 3);
observer2.Subscribe(provider);
provider.BaggageStatus(511, "San Francisco", 2);
provider.BaggageStatus(712);
observer2.Unsubscribe();
provider.BaggageStatus(400);
provider.LastBaggageClaimed();
Imports Observables.Example
Imports System.Threading
Module Program
Sub Main(args As String())
Dim provider As New BaggageHandler()
Dim observer1 As New ArrivalsMonitor("BaggageClaimMonitor1")
Dim observer2 As New ArrivalsMonitor("SecurityExit")
provider.BaggageStatus(712, "Detroit", 3)
observer1.Subscribe(provider)
provider.BaggageStatus(712, "Kalamazoo", 3)
provider.BaggageStatus(400, "New York-Kennedy", 1)
provider.BaggageStatus(712, "Detroit", 3)
observer2.Subscribe(provider)
provider.BaggageStatus(511, "San Francisco", 2)
provider.BaggageStatus(712)
observer2.Unsubscribe()
provider.BaggageStatus(400)
provider.LastBaggageClaimed()
End Sub
End Module