观察者设计模式使订阅者能够向提供程序注册并接收相关通知。 它适合所有需要推送通知的方案。 该模式定义一个提供程序(也称为主题或观察对象)以及零个、一个或多个观察者。 观察者向提供程序注册,并且当任何预定义的条件、事件或状态更改发生时,提供程序就会调用观察者中的一种方法,自动通知所有观察者。 在此方法调用中,提供程序还可以向观察者提供当前的状态信息。 在 .NET Framework 中,通过实现泛型 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 类提供有关到达航班和提取各次航班行李所用转盘的信息。 如以下示例中所示。
using System; using System.Collections.Generic; public class BaggageInfo { private int flightNo; private string origin; private int location; internal BaggageInfo(int flight, string from, int carousel) { this.flightNo = flight; this.origin = from; this.location = carousel; } public int FlightNumber { get { return this.flightNo; } } public string From { get { return this.origin; } } public int Carousel { get { return this.location; } } }
BaggageHandler 类负责接收有关到达航班和行李提取转盘的信息。 在内部,它包含两个集合:
-
observers - 接收更新信息的客户端集合。
-
flights - 航班及其指派的转盘的集合。
两个集合都由 BaggageHandler 类构造函数中实例化的泛型 List<T> 对象表示。 下面的示例中显示 BaggageHandler 类的源代码。
public class BaggageHandler : IObservable<BaggageInfo> { private List<IObserver<BaggageInfo>> observers; private List<BaggageInfo> flights; public BaggageHandler() { observers = new List<IObserver<BaggageInfo>>(); flights = new List<BaggageInfo>(); } public IDisposable Subscribe(IObserver<BaggageInfo> observer) { // Check whether observer is already registered. If not, add it if (! observers.Contains(observer)) { observers.Add(observer); // Provide observer with existing data. foreach (var item in flights) observer.OnNext(item); } return new Unsubscriber<BaggageInfo>(observers, observer); } // Called to indicate all baggage is now unloaded. public void BaggageStatus(int flightNo) { BaggageStatus(flightNo, String.Empty, 0); } public void BaggageStatus(int flightNo, string from, int carousel) { var info = new BaggageInfo(flightNo, from, carousel); // Carousel is assigned, so add new info object to list. if (carousel > 0 && ! flights.Contains(info)) { flights.Add(info); foreach (var observer in observers) observer.OnNext(info); } else if (carousel == 0) { // Baggage claim for flight is done var flightsToRemove = new List<BaggageInfo>(); foreach (var flight in flights) { if (info.FlightNumber == flight.FlightNumber) { flightsToRemove.Add(flight); foreach (var observer in observers) observer.OnNext(info); } } foreach (var flightToRemove in flightsToRemove) flights.Remove(flightToRemove); flightsToRemove.Clear(); } } public void LastBaggageClaimed() { foreach (var observer in observers) observer.OnCompleted(); observers.Clear(); } }
希望接收更新信息的客户端调用 BaggageInfo.Subscribe 方法。 如果客户端之前没有订阅过通知,则会将对客户端 IObserver<T> 实现的引用添加到 observers 集合中。
可以调用重载的 BaggageHandler.BaggageStatus 方法以指示是正在卸载还是已卸载航班行李。 在第一种情况中,向该方法传递航班号、航班起飞的机场,以及卸载行李的转盘。 在第二种情况中,仅向该方法传递航班号。 对于正在卸载的行李,该方法检查传递给方法的 BaggageInfo 信息是否位于 flights 集合中。 如果没有,该方法将添加相应信息并调用每个观察者的 OnNext 方法。 对于完成行李卸载的航班,该方法会检查有关此航班的信息是否位于 flights 集合中。 如果在该集合中,该方法会调用每个观察者的 OnNext 方法并从 flights 集合中移除 BaggageInfo 对象。
当天最后一趟航班着陆并处理完其行李后,将调用 BaggageHandler.LastBaggageClaimed 方法。 该方法调用每个观察者的 OnCompleted 方法以指示已完成所有通知,然后清除 observers 集合。
提供程序的 Subscribe 方法返回使观察者可以在调用 OnCompleted 方法之前停止接收通知的 IDisposable 实现。 下面的示例中显示该 Unsubscriber(Of BaggageInfo) 类的源代码。 当类在 BaggageHandler.Subscribe 方法中实例化时,将向其传递对 observers 集合的引用以及对添加至该集合的观察者的引用。 这些引用将指派给局部变量。 当调用对象的 Dispose 方法时,它会检查观察者是否仍在 observers 集合中,如果在,则移除观察者。
internal class Unsubscriber<BaggageInfo> : IDisposable { private List<IObserver<BaggageInfo>> _observers; private IObserver<BaggageInfo> _observer; internal Unsubscriber(List<IObserver<BaggageInfo>> observers, IObserver<BaggageInfo> observer) { this._observers = observers; this._observer = observer; } public void Dispose() { if (_observers.Contains(_observer)) _observers.Remove(_observer); } }
下面的示例提供名为 ArrivalsMonitor 的 IObserver<T> 实现,它是显示行李提取信息的基类。 该信息按起飞城市名称的字母顺序显示。 ArrivalsMonitor 的方法标记为 overridable (Visual Basic) 或 virtual (C#),所以它们可全部由派生类重写。
using System; using System.Collections.Generic; public class ArrivalsMonitor : IObserver<BaggageInfo> { private string name; private List<string> flightInfos = new List<string>(); private IDisposable cancellation; private string fmt = "{0,-20} {1,5} {2, 3}"; public ArrivalsMonitor(string name) { if (String.IsNullOrEmpty(name)) throw new ArgumentNullException("The observer must be assigned a name."); this.name = name; } public virtual void Subscribe(BaggageHandler provider) { cancellation = provider.Subscribe(this); } public virtual void Unsubscribe() { cancellation.Dispose(); flightInfos.Clear(); } public virtual void OnCompleted() { flightInfos.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 == 0) { var flightsToRemove = new List<string>(); string flightNo = String.Format("{0,5}", info.FlightNumber); foreach (var flightInfo in flightInfos) { if (flightInfo.Substring(21, 5).Equals(flightNo)) { flightsToRemove.Add(flightInfo); updated = true; } } foreach (var flightToRemove in flightsToRemove) flightInfos.Remove(flightToRemove); flightsToRemove.Clear(); } else { // Add flight if it does not exist in the collection. string flightInfo = String.Format(fmt, info.From, info.FlightNumber, info.Carousel); if (! flightInfos.Contains(flightInfo)) { flightInfos.Add(flightInfo); updated = true; } } if (updated) { flightInfos.Sort(); Console.WriteLine("Arrivals information from {0}", this.name); foreach (var flightInfo in flightInfos) Console.WriteLine(flightInfo); Console.WriteLine(); } } }
ArrivalsMonitor 类包括 Subscribe 和 Unsubscribe 方法。 Subscribe 方法允许类将由对 Subscribe 的调用返回的 IDisposable 实现保存至私有变量中。 Unsubscribe 方法允许类通过调用提供程序的 Dispose 实现来取消订阅通知。 ArrivalsMonitor 还提供 OnNext、OnError 和 OnCompleted 方法的实现。 仅 OnNext 实现包含大量代码。 该方法使用已排序的私有泛型 List<T> 对象,此对象维护到达航班的起飞机场及其行李使用的转盘的信息。 如果 BaggageHandler 类报告有新航班到达,则 OnNext 方法实现会将有关该航班的信息添加到列表中。 如果 BaggageHandler 类报告已卸载航班行李,则 OnNext 方法会从列表中移除该航班。 无论何时进行更改,都会对列表进行排序,并将其显示在控制台上。
下面的示例包含实例化 BaggageHandler 类的应用程序入口点,以及两个 ArrivalsMonitor 类的实例,并使用 BaggageHandler.BaggageStatus 方法添加和移除有关到达航班的信息。 在每种情况下,观察者都接收更新并正确显示行李提取信息。
using System; using System.Collections.Generic; public class Example { public static void Main() { BaggageHandler provider = new BaggageHandler(); ArrivalsMonitor observer1 = new ArrivalsMonitor("BaggageClaimMonitor1"); ArrivalsMonitor observer2 = 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(); } } // 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