共用方式為


觀察者設計模式

觀察者設計模式可讓訂閱者向提供者註冊並接收通知。 它適用於任何需要推播式通知的案例。 此模式會定義 提供者 (也稱為 主旨可觀察的)和零、一或多個 觀察者。 觀察者向提供者註冊,每當發生預先定義的條件、事件或狀態變更時,提供者就會叫用委派自動通知所有觀察者。 在此方法呼叫中,提供者也可以提供目前的狀態資訊給觀察者。 在 .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 標準序列運算符,以支援異步程序設計。

實作模式

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

namespace Observables.Example;

public readonly record struct BaggageInfo(
    int FlightNumber,
    string From,
    int Carousel);
Public Class BaggageInfo
    Private flightNo As Integer
    Private origin As String
    Private location As Integer

    Friend Sub New(ByVal flight As Integer, ByVal from As String, ByVal carousel As Integer)
        Me.flightNo = flight
        Me.origin = from
        Me.location = carousel
    End Sub

    Public ReadOnly Property FlightNumber As Integer
        Get
            Return Me.flightNo
        End Get
    End Property

    Public ReadOnly Property From As String
        Get
            Return Me.origin
        End Get
    End Property

    Public ReadOnly Property Carousel As Integer
        Get
            Return Me.location
        End Get
    End Property
End Class

類別 BaggageHandler 負責接收抵達航班和行李提領轉盤的相關信息。 在內部,它會維護兩個集合:

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

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

namespace Observables.Example;

public sealed class BaggageHandler : IObservable<BaggageInfo>
{
    private readonly HashSet<IObserver<BaggageInfo>> _observers = new();
    private readonly HashSet<BaggageInfo> _flights = new();

    public IDisposable Subscribe(IObserver<BaggageInfo> observer)
    {
        // Check whether observer is already registered. If not, add it.
        if (_observers.Add(observer))
        {
            // Provide observer with existing data.
            foreach (BaggageInfo item in _flights)
            {
                observer.OnNext(item);
            }
        }

        return new Unsubscriber<BaggageInfo>(_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);

        // Carousel is assigned, so add new info object to list.
        if (carousel > 0 && _flights.Add(info))
        {
            foreach (IObserver<BaggageInfo> observer in _observers)
            {
                observer.OnNext(info);
            }
        }
        else if (carousel is 0)
        {
            // Baggage claim for flight is done.
            if (_flights.RemoveWhere(
                flight => flight.FlightNumber == info.FlightNumber) > 0)
            {
                foreach (IObserver<BaggageInfo> observer in _observers)
                {
                    observer.OnNext(info);
                }
            }
        }
    }

    public void LastBaggageClaimed()
    {
        foreach (IObserver<BaggageInfo> observer in _observers)
        {
            observer.OnCompleted();
        }

        _observers.Clear();
    }
}
Public Class BaggageHandler : Implements IObservable(Of BaggageInfo)

    Private observers As List(Of IObserver(Of BaggageInfo))
    Private flights As List(Of BaggageInfo)

    Public Sub New()
        observers = New List(Of IObserver(Of BaggageInfo))
        flights = New List(Of BaggageInfo)
    End Sub

    Public Function Subscribe(ByVal observer As IObserver(Of BaggageInfo)) As IDisposable _
                    Implements IObservable(Of BaggageInfo).Subscribe
        ' Check whether observer is already registered. If not, add it
        If Not observers.Contains(observer) Then
            observers.Add(observer)
            ' Provide observer with existing data.
            For Each item In flights
                observer.OnNext(item)
            Next
        End If
        Return New Unsubscriber(Of BaggageInfo)(observers, observer)
    End Function

    ' Called to indicate all baggage is now unloaded.
    Public Sub BaggageStatus(ByVal flightNo As Integer)
        BaggageStatus(flightNo, String.Empty, 0)
    End Sub

    Public Sub BaggageStatus(ByVal flightNo As Integer, ByVal from As String, ByVal carousel As Integer)
        Dim info As New BaggageInfo(flightNo, from, carousel)

        ' Carousel is assigned, so add new info object to list.
        If carousel > 0 And Not flights.Contains(info) Then
            flights.Add(info)
            For Each observer In observers
                observer.OnNext(info)
            Next
        ElseIf carousel = 0 Then
            ' Baggage claim for flight is done
            Dim flightsToRemove As New List(Of BaggageInfo)
            For Each flight In flights
                If info.FlightNumber = flight.FlightNumber Then
                    flightsToRemove.Add(flight)
                    For Each observer In observers
                        observer.OnNext(info)
                    Next
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flights.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        End If
    End Sub

    Public Sub LastBaggageClaimed()
        For Each observer In observers
            observer.OnCompleted()
        Next
        observers.Clear()
    End Sub
End Class

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

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

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

提供者的 Subscribe 方法會傳回一個 IDisposable 的實作,讓觀察者可以在呼叫 OnCompleted 方法之前停止接收通知。 此 Unsubscriber(Of BaggageInfo) 類別的原始碼會顯示在下列範例中。 在 BaggageHandler.Subscribe 方法中具現化類別時,會將 _observers 集合的參考與加入該集合的觀察者參考一併傳遞。 這些參考會指派給局部變數。 呼叫 物件的 Dispose 方法時,它會檢查觀察者是否存在於集合中 _observers ,如果確實存在,則會移除觀察者。

namespace Observables.Example;

internal sealed class Unsubscriber<BaggageInfo> : IDisposable
{
    private readonly ISet<IObserver<BaggageInfo>> _observers;
    private readonly IObserver<BaggageInfo> _observer;

    internal Unsubscriber(
        ISet<IObserver<BaggageInfo>> observers,
        IObserver<BaggageInfo> observer) => (_observers, _observer) = (observers, observer);

    public void Dispose() => _observers.Remove(_observer);
}
Friend Class Unsubscriber(Of BaggageInfo) : Implements IDisposable
    Private _observers As List(Of IObserver(Of BaggageInfo))
    Private _observer As IObserver(Of BaggageInfo)

    Friend Sub New(ByVal observers As List(Of IObserver(Of BaggageInfo)), ByVal observer As IObserver(Of BaggageInfo))
        Me._observers = observers
        Me._observer = observer
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        If _observers.Contains(_observer) Then
            _observers.Remove(_observer)
        End If
    End Sub
End Class

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

namespace Observables.Example;

public class ArrivalsMonitor : IObserver<BaggageInfo>
{
    private readonly string _name;
    private readonly List<string> _flights = new();
    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()
    {
        _cancellation?.Dispose();
        _flights.Clear();
    }

    public virtual void OnCompleted() => _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;

        // Flight has unloaded its baggage; remove from the monitor.
        if (info.Carousel is 0)
        {
            string flightNumber = string.Format("{0,5}", info.FlightNumber);
            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();
        }
    }
}
Public Class ArrivalsMonitor : Implements IObserver(Of BaggageInfo)
    Private name As String
    Private flightInfos As New List(Of String)
    Private cancellation As IDisposable
    Private fmt As String = "{0,-20} {1,5}  {2, 3}"

    Public Sub New(ByVal name As String)
        If String.IsNullOrEmpty(name) Then Throw New ArgumentNullException("The observer must be assigned a name.")

        Me.name = name
    End Sub

    Public Overridable Sub Subscribe(ByVal provider As BaggageHandler)
        cancellation = provider.Subscribe(Me)
    End Sub

    Public Overridable Sub Unsubscribe()
        cancellation.Dispose()
        flightInfos.Clear()
    End Sub

    Public Overridable Sub OnCompleted() Implements System.IObserver(Of BaggageInfo).OnCompleted
        flightInfos.Clear()
    End Sub

    ' No implementation needed: Method is not called by the BaggageHandler class.
    Public Overridable Sub OnError(ByVal e As System.Exception) Implements System.IObserver(Of BaggageInfo).OnError
        ' No implementation.
    End Sub

    ' Update information.
    Public Overridable Sub OnNext(ByVal info As BaggageInfo) Implements System.IObserver(Of BaggageInfo).OnNext
        Dim updated As Boolean = False

        ' Flight has unloaded its baggage; remove from the monitor.
        If info.Carousel = 0 Then
            Dim flightsToRemove As New List(Of String)
            Dim flightNo As String = String.Format("{0,5}", info.FlightNumber)
            For Each flightInfo In flightInfos
                If flightInfo.Substring(21, 5).Equals(flightNo) Then
                    flightsToRemove.Add(flightInfo)
                    updated = True
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flightInfos.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        Else
            ' Add flight if it does not exist in the collection.
            Dim flightInfo As String = String.Format(fmt, info.From, info.FlightNumber, info.Carousel)
            If Not flightInfos.Contains(flightInfo) Then
                flightInfos.Add(flightInfo)
                updated = True
            End If
        End If
        If updated Then
            flightInfos.Sort()
            Console.WriteLine("Arrivals information from {0}", Me.name)
            For Each flightInfo In flightInfos
                Console.WriteLine(flightInfo)
            Next
            Console.WriteLine()
        End If
    End Sub
End Class

類別 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();

// Sample output:
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   
//   Arrivals information from BaggageClaimMonitor1
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from SecurityExit
//   Detroit                712    3
//   Kalamazoo              712    3
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from BaggageClaimMonitor1
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from SecurityExit
//   New York-Kennedy       400    1
//   San Francisco          511    2
//   
//   Arrivals information from BaggageClaimMonitor1
//   San Francisco          511    2
Module Example
    Public Sub Main()
        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
' The example displays the following output:
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'
'      Arrivals information from BaggageClaimMonitor1
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from SecurityExit
'      Detroit                712    3
'      Kalamazoo              712    3
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from BaggageClaimMonitor1
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from SecurityExit
'      New York-Kennedy       400    1
'      San Francisco          511    2
'
'      Arrivals information from BaggageClaimMonitor1
'      San Francisco          511    2
標題 說明
觀察者設計模式最佳做法 描述開發實作觀察者設計模式的應用程式時要採用的最佳做法。
如何實作提供者 提供溫度監控應用程式的方案提供者的逐步實作。
如何實作觀察者 為溫度監視應用程式提供觀察者的逐步實作。