监视程序设计模式
观察者设计模式使订阅者能够从提供程序注册并接收通知。 它适用于需要基于推送的通知的任何方案。 此模式定义提供程序(亦称为“使用者”或“可观察对象”),以及零个、一个或多个观察者。 观察者注册提供程序,并且每当预定义的条件、事件或状态发生更改时,该提供程序会通过调用委托来自动通知所有观察者。 在此方法调用中,该提供程序还可向观察者提供当前状态信息。 在 .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
相关文章
Title | 描述 |
---|---|
监视程序设计模式最佳做法 | 描述开发实现观察者设计模式的应用程序时要采用的最佳做法。 |
如何:实现提供程序 | 为温度监控应用程序提供一个提供程序的分步实现。 |
如何:实现监视程序 | 为温度监控应用程序提供一个观察者的分步实现。 |