觀察者設計模式可讓訂閱者向提供者註冊並接收通知。 它適用於任何需要推播式通知的案例。 此模式會定義 提供者 (也稱為 主旨 或 可觀察的)和零、一或多個 觀察者。 觀察者向提供者註冊,當預設條件、事件或狀態變化發生時,提供者會自動透過呼叫代理人通知所有觀察者。 在此方法呼叫中,提供者也可以提供目前的狀態資訊給觀察者。 在 .NET 中,觀察者設計模式會藉由實作泛型 System.IObservable<T> 和 System.IObserver<T> 介面來套用。 泛型型別參數代表提供通知資訊的型別。
套用模式的時機
觀察者設計模式適用於分散式推播式通知,因為它支援兩個不同元件或應用層之間的全新分隔,例如數據源(商業規則)層和使用者介面(顯示)層。 每當提供者使用回呼函式來提供其用戶端目前資訊時,都可以實作設計模式。
實作模式需要您提供下列詳細資料:
提供者或主體,它是傳送通知給觀察者的對象。 提供者是實作 介面的 IObservable<T> 類別或結構。 提供者必須實作單一方法, IObservable<T>.Subscribe這個方法是由想要從提供者接收通知的觀察者所呼叫。
觀察者,這是接收提供者通知的物件。 觀察者(Observer)是實作介面IObserver<T>的類別或是結構。 觀察者必須實作三種方法,所有方法都是由提供者呼叫:
- IObserver<T>.OnNext,提供觀察者新的或目前的資訊。
- IObserver<T>.OnError,告知觀察者發生錯誤。
- IObserver<T>.OnCompleted,表示提供者已完成傳送通知。
可讓提供者追蹤觀察者的機制。 一般而言,提供者會使用容器物件,例如 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 包含 Subscribe 和 Unsubscribe 方法。
Subscribe方法可讓 類別儲存IDisposable對私用變數呼叫Subscribe所傳回的實作。
Unsubscribe方法可讓 類別呼叫提供者的Dispose實作,以取消訂閱通知。
ArrivalsMonitor也提供OnNext、OnError和OnCompleted方法的實作。 只有實作 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