觀察者設計模式
觀察者設計模式可讓訂閱者向提供者註冊,並且接收通知。 它適合任何需要推入型通知的情節。 這個模式會定義「提供者」(也稱為「主題」或「可預見值」),以及零個、一個或多個「觀察者」。 觀察者會向提供者註冊,而且只要預先定義的條件、事件或狀態有所變更,提供者就會自動叫用委派,來通知所有觀察者。 在這個方法呼叫中,提供者也可以提供目前的狀態資訊給觀察者。 在 .NET 中,觀察者設計模式是透過實作泛型 System.IObservable<T> 和 System.IObserver<T> 介面來套用。 泛型型別參數代表提供通知資訊的類型。
套用模式的時機
觀察者設計模式適合分散式推入型通知,因為它支援清楚分隔兩種不同的元件或應用程式層,例如資料來源 (商務邏輯) 層和使用者介面 (顯示) 層。 當提供者使用回呼將目前的資訊提供給其用戶端時,便會實作這個模式。
實作模式需要您提供下列詳細資料:
提供者或主題,也就是傳送通知給觀察者的物件。 提供者是實作 IObservable<T> 介面的類別或結構。 提供者必須實作單一方法 IObservable<T>.Subscribe,觀察者會呼叫該方法,以接收來自提供者的通知。
觀察者,也就是接收來自提供者之通知的物件。 觀察者是實作 IObserver<T> 介面的類別或結構。 觀察者必須實作三種方法,這三種方法全都是由提供者呼叫:
- IObserver<T>.OnNext,提供新的或目前的資訊給觀察者。
- IObserver<T>.OnError,通知觀察者發生錯誤。
- IObserver<T>.OnCompleted,指出提供者已完成傳送通知。
允許提供者追蹤觀察者的機制。 一般而言,提供者會使用容器物件 (例如 System.Collections.Generic.List<T> 物件) 保留已訂閱通知的 IObserver<T> 實作參考。 基於這個目的使用儲存容器,可讓提供者處理零個到無限多個觀察者。 觀察者接收通知的順序並未定義;提供者可自由使用任何方法來決定順序。
IDisposable 實作,可讓提供者在通知完成時移除觀察者。 觀察者會從 Subscribe 方法接收 IDisposable 實作的參考,如此觀察者同樣可以在提供者完成傳送通知之前呼叫 IDisposable.Dispose 方法來取消訂閱。
包含提供者傳送給觀察者之資料的物件。 這個物件的類型對應至 IObservable<T> 和 IObserver<T> 介面的泛型類型參數。 雖然這個物件可以與 IObservable<T> 實作相同,但它常是不同的類型。
注意
除了實作觀察者設計模式之外,您可能對探索使用 IObservable<T> 和 IObserver<T> 介面建置的程式庫有興趣。 例如,Reactive Extensions for .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
方法,並且從 _flights
集合中移除 BaggageInfo
物件。
當天最後一個班機降落且其行李已處理完成時,就會呼叫 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
下列範例提供名為 ArrivalsMonitor
的 IObserver<T> 實作,其為顯示行李提領資訊的基底類別。 資訊會依照起飛城市名稱的英文字母順序顯示。 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
類別包含 Subscribe
和 Unsubscribe
方法。 Subscribe
方法可讓類別將呼叫 Subscribe 所傳回的 IDisposable 實作儲存至私用變數。 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();
// 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
相關文章
標題 | 描述 |
---|---|
觀察者設計模式最佳做法 | 描述開發實作觀察者設計模式的應用程式時,所採用的最佳做法。 |
操作說明:實作提供者 | 提供溫度監控應用程式的提供者逐步實作。 |
操作說明:實作觀察者 | 提供溫度監控應用程式的觀察者逐步實作。 |