Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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:
- IObserver<T>.OnNext, som förser observatören med ny eller aktuell information.
- IObserver<T>.OnError, som informerar observatören om att ett fel har inträffat.
- IObserver<T>.OnCompleted, vilket anger att leverantören har avslutat att skicka meddelanden.
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.Reactivei stället för att implementeraIObservable<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