觀察者設計模式

觀察者設計模式可讓訂閱者向提供者註冊並接收通知。 它適用於任何需要推播式通知的案例。 此模式會定義 提供者 (也稱為 主旨可觀察的)和零、一或多個 觀察者。 觀察者向提供者註冊,當預設條件、事件或狀態變化發生時,提供者會自動透過呼叫代理人通知所有觀察者。 在此方法呼叫中,提供者也可以提供目前的狀態資訊給觀察者。 在 .NET 中,觀察者設計模式會藉由實作泛型 System.IObservable<T>System.IObserver<T> 介面來套用。 泛型型別參數代表提供通知資訊的型別。

套用模式的時機

觀察者設計模式適用於分散式推播式通知,因為它支援兩個不同元件或應用層之間的全新分隔,例如數據源(商業規則)層和使用者介面(顯示)層。 每當提供者使用回呼函式來提供其用戶端目前資訊時,都可以實作設計模式。

實作模式需要您提供下列詳細資料:

  • 提供者或主體,它是傳送通知給觀察者的對象。 提供者是實作 介面的 IObservable<T> 類別或結構。 提供者必須實作單一方法, IObservable<T>.Subscribe這個方法是由想要從提供者接收通知的觀察者所呼叫。

  • 觀察者,這是接收提供者通知的物件。 觀察者(Observer)是實作介面IObserver<T>的類別或是結構。 觀察者必須實作三種方法,所有方法都是由提供者呼叫:

  • 可讓提供者追蹤觀察者的機制。 一般而言,提供者會使用容器物件,例如 System.Collections.Generic.List<T> 物件,來保存訂閱通知之 IObserver<T> 實作的參考。 使用此用途的記憶體容器可讓提供者處理零到無限數目的觀察者。 觀察者接收通知的順序未定義;提供者可以使用任何方法來判斷順序。

  • IDisposable 作,可讓提供者在通知完成時移除觀察者。 觀察者會從IDisposable方法接收到Subscribe實作的參考,因此可以在提供者完成傳送通知之前,呼叫IDisposable.Dispose方法以取消訂閱。

  • 包含供應者傳送給觀察者的資料的物件。 這個物件的型別會對應至 IObservable<T>IObserver<T> 介面的泛型型別參數。 雖然這個物件可以和實作 IObservable<T> 相同,但最常見的是個別的類型。

備註

除了實作觀察者設計模式外,你也可以考慮探索使用 IObservable<T>IObserver<T> 介面來建構的函式庫。 例如, 適用於 .NET 的回應式延伸模組 (Rx) 包含一組擴充方法和 LINQ 標準序列運算符,以支援異步程序設計。

何時考慮替代方案

IObservable<T> / IObserver<T> 介面非常適合推播式通知情境,但.NET 提供了其他可能更適合的模式:

  • 標準.NET事件 — 針對單一應用程式內的簡單通知情境,events更具慣用法且更容易實作。
  • IAsyncEnumerable<T> — 對於非同步拉取序列,消費者可控制節奏,請使用 非同步串流
  • System.Threading.Channels — 對於具有背壓與非同步支援的生產者-消費者模式,請使用 System.Threading.Channels
  • Reactive Extensions (Rx.NET) — 對於複雜的事件組合、過濾與轉換,請使用 System.Reactive 套件,而非直接實作 IObservable<T>

在 .NET 中,IObservable<T> 最主要的用途是 DiagnosticListener,它可讓框架與程式庫作者發出結構化的診斷事件,供使用者訂閱。

實作模式

下列範例使用觀察者設計模式來實作機場行李理賠信息系統。 類別 BaggageInfo 提供關於抵達航班和行李轉盤的資訊,每個航班的行李都可以在這些轉盤上領取。 如下列範例所示。

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 負責接收抵達航班和行李提領轉盤的相關信息。 在內部,它會維護兩個集合:

  • _observers:觀察更新資訊的用戶端集合。
  • _flights:航班及其指定的旋轉木馬集合。

類別的 BaggageHandler 原始碼會顯示在下列範例中。

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

想要接收更新資訊的用戶端會呼叫 BaggageHandler.Subscribe 方法。 如果用戶端先前尚未訂閱通知,則會將客戶端實作的 IObserver<T> 參考新增至 _observers 集合。

您可以呼叫多載 BaggageHandler.BaggageStatus 方法,以指出航班中的行李正在卸載或不再卸除。 在第一個案例中,方法會傳遞航班號碼、航班起始的機場,以及正在卸除行李的行李轉盤。 在第二個案例中,該方法僅接收一個航班號碼。 對於正在卸貨的行李,方法會檢查傳遞給方法的 BaggageInfo 資訊是否存在於 _flights 集合中。 如果沒有,方法會新增資訊,並呼叫每個觀察者的 OnNext 方法。 對於行李已不再卸載的航班,此方法會檢查該航班的資訊是否儲存在 _flights 集合中。 如果是,方法會呼叫每個觀察者的 OnNext 方法,並從集合中移除 BaggageInfo 物件 _flights

當當天的最後一班航班降落,並且其行李已完成處理時,便會呼叫 BaggageHandler.LastBaggageClaimed 方法。 這個方法會呼叫每個觀察者的 OnCompleted 方法,指出所有通知都已完成,然後清除 _observers 集合。

提供者的 Subscribe 方法會傳回一個 IDisposable 的實作,讓觀察者可以在呼叫 OnCompleted 方法之前停止接收通知。 此 Unsubscriber 類別的原始碼會顯示在下列範例中。 當類別在 BaggageHandler.Subscribe 方法中實例化時,會傳入 _lock 物件的參考、_observers 集合的參考,以及加入該集合之觀察者的參考。 這些參考會指派給局部變數。 當 Dispose 物件的方法被呼叫時,會將觀察者從 _observers 鎖內的集合中移除。

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

下列範例提供名為IObserver<T>ArrivalsMonitor實作,該基類用於顯示行李領取資訊。 資訊會依原始城市的名稱依字母順序顯示。 方法 ArrivalsMonitor 會標示為 overridable(在 Visual Basic 中)或 virtual(在 C# 中),因此可以在衍生類別中覆寫。

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 包含 SubscribeUnsubscribe 方法。 Subscribe方法可讓 類別儲存IDisposable對私用變數呼叫Subscribe所傳回的實作。 Unsubscribe方法可讓 類別呼叫提供者的Dispose實作,以取消訂閱通知。 ArrivalsMonitor也提供OnNextOnErrorOnCompleted方法的實作。 只有實作 OnNext 包含大量的程序代碼。 此方法適用於私人、已排序的泛型List<T> 物件,該對象會維護抵達航班的起始機場和行李轉盤的相關資訊。 如果BaggageHandler類別報告新的航班到達,方法OnNext的實作會將該航班的詳細信息新增至清單。 如果類別 BaggageHandler 報告航班的行李已卸除,方法 OnNext 會從清單中移除該航班。 每當進行變更時,清單就會排序並顯示至主控台。

下列範例包含應用程序進入點,可具現化 BaggageHandler 類別和 類別的 ArrivalsMonitor 兩個實例,並使用 BaggageHandler.BaggageStatus 方法來新增和移除抵達航班的相關信息。 在每個情況下,觀察者會收到更新,並正確地顯示行李領取資訊。

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