Nyelv
Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
A megfigyelői tervezési minta lehetővé teszi az előfizető számára, hogy regisztráljon egy szolgáltatónál, és értesítéseket kapjon. Bármely olyan forgatókönyvhöz alkalmas, amely leküldéses alapú értesítést igényel. A minta egy szolgáltatót (más néven tárgyat vagy megfigyelhetőt) és nulla, egy vagy több megfigyelőt határoz meg. A megfigyelők regisztrálnak a szolgáltatónál, és amikor előre meghatározott állapot, esemény vagy állapotváltozás történik, a szolgáltató automatikusan értesíti az összes megfigyelőt egy meghatalmazott meghívásával. Ebben a metódushívásban a szolgáltató az aktuális állapotinformációkat is megadhatja a megfigyelőknek. A .NET-ben a megfigyelői tervezési minta az általános System.IObservable<T> és System.IObserver<T> interfészek implementálásával lesz alkalmazva. Az általános típusparaméter az értesítési információkat biztosító típust jelöli.
Mikor kell alkalmazni a mintát?
A megfigyelői tervezési minta alkalmas elosztott leküldéses értesítésekre, mivel támogatja a két különböző összetevő vagy alkalmazásréteg, például az adatforrás (üzleti logika) réteg és a felhasználói felület (megjelenítés) réteg tiszta elkülönítését. A minta akkor implementálható, ha egy szolgáltató visszahívásokkal látja el ügyfeleit az aktuális információkkal.
A minta implementálásához a következő adatokat kell megadnia:
Egy szolgáltató vagy tárgy, amely az az objektum, amely értesítéseket küld a megfigyelőknek. A szolgáltató az interfészt megvalósító IObservable<T> osztály vagy struktúra. A szolgáltatónak egyetlen módszert kell implementálnia, IObservable<T>.Subscribeamelyet megfigyelők hívnak meg, akik értesítést szeretnének kapni a szolgáltatótól.
Megfigyelő, amely egy olyan objektum, amely értesítéseket fogad egy szolgáltatótól. A megfigyelő az interfészt megvalósító IObserver<T> osztály vagy struktúra. A megfigyelőnek három módszert kell alkalmaznia, amelyek mindegyiket a szolgáltató hívja meg:
- IObserver<T>.OnNext, amely új vagy aktuális információkkal látja el a megfigyelőt.
- IObserver<T>.OnError, amely tájékoztatja a megfigyelőt, hogy hiba történt.
- IObserver<T>.OnCompleted, amely azt jelzi, hogy a szolgáltató befejezte az értesítések küldését.
Egy mechanizmus, amely lehetővé teszi a szolgáltató számára a megfigyelők nyomon követését. A szolgáltató általában egy tárolóobjektumot( például objektumot System.Collections.Generic.List<T> ) használ az IObserver<T> értesítésekre előfizetett implementációkra mutató hivatkozások tárolására. A tárolótartály e célra történő használata lehetővé teszi a szolgáltató számára, hogy nullától korlátlan számú megfigyelőt kezeljen. Nincs meghatározva az a sorrend, amelyben a megfigyelők értesítéseket kapnak; a szolgáltató szabadon használhat bármilyen módszert a megrendelés meghatározásához.
Olyan IDisposable implementáció, amely lehetővé teszi a szolgáltató számára, hogy az értesítés befejezésekor eltávolítsa a megfigyelőket. A megfigyelők a IDisposable metódusból kapnak egy hivatkozást a Subscribe megvalósításra, így leiratkozhatnak a IDisposable.Dispose metódus meghívásával, mielőtt a szolgáltató befejezte az értesítések küldését.
Olyan objektum, amely a szolgáltató által a megfigyelőknek küldött adatokat tartalmazza. Ennek az objektumnak a típusa megfelel a IObservable<T> és IObserver<T> interfészek általános típusparaméterének. Bár ez az objektum megegyezhet az IObservable<T> implementációval, általában külön típus.
Megjegyzés:
A megfigyelő tervezési minta megvalósítása mellett érdemes lehet megismerni azokat a kódtárakat is, amelyek a IObservable<T> és IObserver<T> interfészekre épülnek. A .NET -hez (Rx) készült reaktív bővítmények például bővítménymetória- és LINQ-szabványütemezési operátorokból állnak, amelyek támogatják az aszinkron programozást.
Mikor érdemes megfontolni az alternatívákat?
A IObservable<T>/IObserver<T> interfészek kiválóan alkalmasak leküldéses értesítési forgatókönyvekhez, de .NET más mintákat is kínál, amelyek jobban illeszkedhetnek:
- A .NET események – Az egyetlen alkalmazáson belüli egyszerű értesítési forgatókönyvek esetében a events inkább idiomatikusak és könnyebben implementálhatóak.
-
IAsyncEnumerable<T>— Az aszinkron lekéréses sorozatok esetében, ahol a fogyasztó vezérli a tempót, aszinkron streameket használjon. -
System.Threading.Channels— A háttérnyomással és aszinkron támogatással rendelkező termelői-fogyasztói mintákhoz használja a következőt System.Threading.Channels: . -
Reactive Extensions (Rx.NET) – Összetett eseményösszeállításhoz, szűréshez és átalakításhoz használja a
System.Reactivecsomagot aIObservable<T>közvetlen implementálása helyett.
A IObservable<T> legjelentősebb felhasználása a .NET-ben a DiagnosticListener, amely lehetővé teszi, hogy a keretrendszerek és könyvtárak szerzői strukturált diagnosztikai eseményeket tegyenek közzé, amelyekre a felhasználók feliratkozhatnak.
A minta implementálása
Az alábbi példa a megfigyelői tervezési mintát használja egy repülőtéri poggyászigénylési információs rendszer implementálásához. Az BaggageInfo osztály információkat nyújt az érkező járatokról és a körhintákról, ahol az egyes járatok poggyászai átvehetők. Az alábbi példában látható.
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
Az BaggageHandler osztály feladata, hogy információkat fogadjon az érkező járatokról és a poggyászkiadó szalagokról. Belsőleg két gyűjteményt tart fenn:
-
_observers: A frissített információkat figyelő ügyfelek gyűjteménye. -
_flights: A járatok és a hozzájuk rendelt körhinták gyűjteménye.
Az osztály forráskódja BaggageHandler az alábbi példában látható.
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
Azok az ügyfelek, akik frissített információkat szeretnének kapni, hívják meg a metódust BaggageHandler.Subscribe . Ha az ügyfél korábban nem iratkozott fel értesítésekre, a rendszer hozzáad egy hivatkozást az ügyfél implementációjára IObserver<T> a _observers gyűjteményhez.
A túlterhelt BaggageHandler.BaggageStatus módszer arra utalhat, hogy a járatról érkező poggyászt vagy kipakolják, vagy már nem rakodják ki. Az első esetben a módszer megkapja a járatszámot, a repülőteret, ahonnan a járat indult, és a körhintát, ahol a poggyászt kirakják. A második esetben a metódus csak egy járatszámot ad át. A kipakolt poggyászok esetében a módszer ellenőrzi, hogy a BaggageInfo módszernek átadott információk léteznek-e a _flights gyűjteményben. Ha nem ez a helyzet, a metódus hozzáadja az információkat, és meghívja minden egyes megfigyelő OnNext metódusát. Azon járatok esetében, amelyek poggyászát már nem ürítik ki, a módszer ellenőrzi, hogy az adott járat információi a _flights gyűjteményben találhatók-e. Ha igen, a metódus meghívja az egyes megfigyelők metódusát OnNext , és eltávolítja az BaggageInfo objektumot a _flights gyűjteményből.
Amikor a nap utolsó járata leszállt, és a poggyászát feldolgozták, a BaggageHandler.LastBaggageClaimed módszer meghívása történik. Ez a metódus meghívja az egyes megfigyelők metódusát OnCompleted , hogy jelezze, hogy az összes értesítés befejeződött, majd törli a gyűjteményt _observers .
A szolgáltató metódusa Subscribe olyan implementációt IDisposable ad vissza, amely lehetővé teszi, hogy a megfigyelők ne kapjanak értesítéseket a OnCompleted metódus meghívása előtt. Az osztály forráskódja Unsubscriber az alábbi példában látható. Amikor az osztályt példányosítják a BaggageHandler.Subscribe metódusban, átadják neki az _lock objektumra mutató hivatkozást, a _observers gyűjteményt, valamint a gyűjteményhez hozzáadott megfigyelőre mutató hivatkozást. Ezek a hivatkozások helyi változókhoz vannak rendelve. Amikor az objektum Dispose metódusát meghívják, zárolás alatt eltávolítja a megfigyelőt a _observers gyűjteményből.
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
Az alábbi példa egy IObserver<T> olyan implementációt mutat be, amelynek neve ArrivalsMonitoregy alaposztály, amely megjeleníti a poggyászigénylési adatokat. Az információk betűrendben jelennek meg az eredeti város nevével. A metódusok ArrivalsMonitoroverridable (Visual Basicben) vagy virtual (C#-ban) jelöléssel rendelkeznek, így felülírhatók egy származtatott osztályban.
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
Az ArrivalsMonitor osztály tartalmazza a Subscribe és a Unsubscribe metódusokat. A Subscribe metódus lehetővé teszi az osztály számára, hogy mentse a IDisposable magánváltozóra Subscribe irányuló hívás által visszaadott implementációt. Ezzel Unsubscribe a módszerrel az osztály leiratkozhat az értesítésekről a szolgáltató implementációjának meghívásával Dispose .
ArrivalsMonitor metódusok implementációját is biztosítja: OnNext, OnError, és OnCompleted. Csak az OnNext implementáció tartalmaz jelentős mennyiségű kódot. A módszer egy privát, rendezett, általános List<T> objektummal működik, amely információkat tart fenn az érkező járatok származási repülőtereiről és azokról a körhintákról, amelyeken a poggyászuk elérhető. Ha az BaggageHandler osztály új járat érkezését jelenti, a metódus implementációja OnNext hozzáadja az adott járat adatait a listához. Ha az BaggageHandler osztály azt jelenti, hogy a járat poggyásza ki lett rakodva, a OnNext módszer eltávolítja a járatot a listáról. Amikor módosítást végez, a rendszer rendezi és megjeleníti a listát a konzolon.
Az alábbi példa tartalmazza a BaggageHandler osztályt példányosító alkalmazásbelépési pontot, továbbá a ArrivalsMonitor osztály két példányát, és a BaggageHandler.BaggageStatus módszerrel adatokat ad hozzá és távolít el az érkező járatokról. A megfigyelők minden esetben frissítéseket kapnak, és helyesen jelenítik meg a poggyászigénylési információkat.
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