Gözlemci tasarım deseni

Gözlemci tasarım düzeni, abonenin sağlayıcıya kaydolmasını ve sağlayıcıdan bildirim almasını sağlar. Anında iletme tabanlı bildirim gerektiren tüm senaryolar için uygundur. Desen bir sağlayıcı ( konu veya gözlemlenebilir olarak da bilinir) ve sıfır, bir veya daha fazla gözlemci tanımlar. Gözlemciler sağlayıcıya kaydolup önceden tanımlanmış bir koşul, olay veya durum değişikliği gerçekleştiğinde, sağlayıcı bir temsilci çağırarak tüm gözlemcilere otomatik olarak bildirim gönderir. Bu yöntem çağrısında sağlayıcı, gözlemcilere geçerli durum bilgilerini de sağlayabilir. .NET'te, gözlemci tasarım deseni genel System.IObservable<T> ve System.IObserver<T> arabirimler uygulanarak uygulanır. Genel tür parametresi, bildirim bilgilerini sağlayan türü temsil eder.

Desen ne zaman uygulanır?

Gözlemci tasarım deseni, veri kaynağı (iş mantığı) katmanı ve kullanıcı arabirimi (görüntüleme) katmanı gibi iki farklı bileşen veya uygulama katmanı arasında temiz bir ayrımı desteklediğinden dağıtılmış anında iletme tabanlı bildirimler için uygundur. Sağlayıcı, istemcilerine anlık bilgileri sağlamak için geri aramaları kullandığında bu desen uygulanabilir.

Deseni uygulamak için aşağıdaki ayrıntıları sağlamanız gerekir:

  • Gözlemcilere bildirim gönderen nesne olan sağlayıcı veya konu. Sağlayıcı, arabirimini uygulayan IObservable<T> bir sınıf veya yapıdır. Sağlayıcı, gözlemciler tarafından çağrılan ve sağlayıcıdan bildirim almak isteyen tek bir yöntem olan IObservable<T>.Subscribe'yi uygulamalıdır.

  • Bir sağlayıcıdan bildirim alan bir nesne olan gözlemci. Gözlemci, arabirimini uygulayan IObserver<T> bir sınıf veya yapıdır. Gözlemcinin üç yöntem uygulaması gerekir ve bunların tümü sağlayıcı tarafından çağrılır:

  • Sağlayıcının gözlemcileri izlemesine olanak tanıyan bir mekanizma. Genellikle sağlayıcı, bildirimlere abone olan System.Collections.Generic.List<T> uygulamalarının referanslarını tutmak için IObserver<T> nesne gibi bir kapsayıcı nesne kullanır. Bu amaçla bir depolama kapsayıcısı kullanmak, sağlayıcının sıfırdan sınırsız sayıda gözlemciye kadar işlemesini sağlar. Gözlemcilerin bildirim alma sırası tanımlanmamıştır; sağlayıcı, siparişi belirlemek için herhangi bir yöntemi kullanabilir.

  • IDisposable Bildirim tamamlandığında sağlayıcının gözlemcileri kaldırmasını sağlayan bir uygulama. Gözlemciler, IDisposable uygulamasına bir başvuru almak için Subscribe yöntemini kullanır, böylece sağlayıcı bildirim göndermeyi tamamlamadan önce abonelikten çıkmak için IDisposable.Dispose yöntemini de çağırabilirler.

  • Sağlayıcının gözlemcilerine gönderdiği verileri içeren bir nesne. Bu nesnenin türü, IObservable<T> ve IObserver<T> arabirimlerinin genel tür parametresine karşılık gelir. Bu nesne uygulamayla IObservable<T> aynı olsa da, en yaygın olarak ayrı bir tür olur.

Uyarı

Gözlemci tasarım desenini uygulamanın yanı sıra, IObservable<T> ve IObserver<T> arabirimleri kullanılarak oluşturulmuş kitaplıkları keşfetmek de ilginizi çekebilir. Örneğin, .NET (Rx) için Reaktif Uzantılar , zaman uyumsuz programlamayı desteklemek için bir dizi uzantı yönteminden ve LINQ standart dizi işleçlerinden oluşur.

Alternatifler ne zaman dikkate alınır?

IObservable<T> / IObserver<T> arabirimleri, anında iletme tabanlı bildirim senaryoları için çok uygundur, ancak .NET daha uygun olabilecek başka desenler sunar:

  • Standard .NET olayları — Tek bir uygulama içindeki basit bildirim senaryoları için events daha idiyomatiktir ve uygulanması daha kolaydır.
  • IAsyncEnumerable<T> — Tüketicinin hızı kontrol ettiği zamanuyumsuz çekme temelli diziler için zamanuyumsuz akışları kullanın.
  • System.Threading.Channels — Backpressure ve async desteğine sahip üretici-tüketici desenleri için kullanın System.Threading.Channels.
  • Reactive Extensions (Rx.NET) — Karmaşık olay oluşturma, filtreleme ve dönüştürme için doğrudan System.Reactive uygulamak yerine IObservable<T> paketini kullanın.

.NET'da IObservable<T>'nin en önemli kullanımı, çerçeve ve kitaplık yazarlarının kullanıcıların abone olabildiği yapılandırılmış tanılama olayları yayımlamasına olanak tanıyan DiagnosticListener'dir.

Deseni uygulama

Aşağıdaki örnek, bir havaalanı bagaj talebi bilgi sistemi uygulamak için gözlemci tasarım desenini kullanır. Sınıf BaggageInfo, gelen uçuşlar ve her uçuşun bagajlarının teslim alınabileceği bantlar hakkında bilgi sağlar. Aşağıdaki örnekte gösterilmiştir.

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

BaggageHandler sınıfı, gelen uçuşlar ve bagaj talep bantları hakkında bilgi almaktan sorumludur. Dahili olarak iki koleksiyon tutar:

  • _observers: Güncellenmiş bilgileri gözlemleyen istemcilerden oluşan bir koleksiyon.
  • _flights: Uçuşlar ve bunların atanmış kayar bantları koleksiyonu.

Sınıfın BaggageHandler kaynak kodu aşağıdaki örnekte gösterilmiştir.

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

Güncelleştirilmiş bilgileri almak isteyen istemciler yöntemini çağırır BaggageHandler.Subscribe . İstemci daha önce bildirimlere abone olmadıysa, istemcinin kendi IObserver<T> uygulamasına bir başvuru, _observers koleksiyonuna eklenir.

Aşırı yüklenmiş BaggageHandler.BaggageStatus metodu, bir uçuştan gelen bagajların boşaltılmakta olduğunu veya artık boşaltılmadığını belirtmek için çağrılabilir. İlk durumda, metoda bir uçuş numarası, uçuşun başladığı havaalanı ve bagajların boşaltıldığı döner bant iletilir. İkinci durumda, metoda yalnızca bir uçuş numarası aktarılır. Boşaltılan bagajlar için, yönteme aktarılan BaggageInfo bilgilerin _flights koleksiyonda mevcut olup olmadığını yöntem denetler. Eğer bu gerçekleşmezse, yöntem bilgileri ekler ve her gözlemcinin OnNext yöntemini çağırır. Bagajları kaldırılmayan uçuşlar için yöntem, söz konusu uçuşla ilgili bilgilerin koleksiyonda _flights depolanıp depolanmadığını denetler. Bu durumda, yöntem her gözlemcinin OnNext yöntemini çağırır ve BaggageInfo nesnesini _flights koleksiyonundan kaldırır.

Günün son uçuşu indiğinde ve bagajı işlendiğinde, BaggageHandler.LastBaggageClaimed yöntemi çağrılır. Bu yöntem, tüm bildirimlerin tamamlandığını belirtmek için her gözlemcinin OnCompleted yöntemini çağırır ve ardından koleksiyonu temizler _observers .

Sağlayıcının Subscribe yöntemi, IDisposable yöntemi çağrılmadan önce gözlemcilerin bildirim almayı durdurmasını sağlayan bir OnCompleted uygulama döndürür. Bu Unsubscriber sınıfın kaynak kodu aşağıdaki örnekte gösterilmiştir. Sınıf, BaggageHandler.Subscribe yönteminde örneklendirildiğinde, kendisine _lock nesnesine bir başvuru, _observers koleksiyonu ve koleksiyona eklenen gözlemciye bir başvuru geçirilir. Bu referanslar yerel değişkenlere atanır. Nesnenin Dispose yöntemi çağrıldığında, gözlemciyi kilit altında _observers koleksiyonundan kaldırır.

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

Aşağıdaki örnek, bagaj talep bilgilerini görüntüleyen bir temel sınıf olan adlı IObserver<T>bir ArrivalsMonitor uygulama sağlar. Bilgiler, kaynak şehir adıyla alfabetik olarak görüntülenir. ArrivalsMonitor yöntemleri overridable (Visual Basic'te) veya virtual (C# dilinde) olarak işaretlenir, bu nedenle türetilmiş bir sınıfta geçersiz kılınabilir.

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

ArrivalsMonitor sınıfı, Subscribe ve Unsubscribe yöntemlerini içerir. Subscribe yöntemi, IDisposable çağrısının döndürdüğü Subscribe uygulamasını özel bir değişkene kaydetmesi için sınıfa olanak tanır. yöntemi, Unsubscribe sınıfının sağlayıcının Dispose uygulamasını çağırarak bildirim aboneliğini kaldırmasını sağlar. ArrivalsMonitorayrıca , OnNextve OnError yöntemlerinin OnCompleteduygulamalarını sağlar. OnNext Yalnızca uygulama önemli miktarda kod içerir. Yöntem, varış uçuşlarının kalkış havaalanları ve bagajlarının bulunduğu döner platformlar hakkında bilgi sağlayan özel, sıralanmış, genel List<T> bir nesneyle çalışır. BaggageHandler Sınıf yeni bir uçuş gelişini bildirirse, OnNext yöntem uygulaması bu uçuşla ilgili bilgileri listeye ekler. Sınıf, BaggageHandler uçuşun bagajının boşaltıldığını bildirirse, OnNext yöntem bu uçuşu listeden kaldırır. Her değişiklik yapıldığında, liste sıralanır ve konsolda görüntülenir.

Aşağıdaki örnek, BaggageHandler sınıfını ve ArrivalsMonitor sınıfının iki örneğini oluşturan uygulama giriş noktasını içerir ve gelen uçuşlarla ilgili bilgileri eklemek ve kaldırmak için BaggageHandler.BaggageStatus yöntemini kullanır. Her durumda, gözlemciler güncelleştirmeleri alır ve bagaj talep bilgilerini doğru bir şekilde görüntüler.

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