Nyelv

Megfigyelő tervezési minta

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:

  • 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.Reactive csomagot a IObservable<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