在 .NET 中实现观察者模式(3 种技术)






4.99/5 (32投票s)
在 .NET 中实现观察者模式的多种技术
引言
在我们软件开发生涯的这些年里,我们都读过关于设计模式的知识。在所有模式中,我遇到的最酷、最实用的莫过于观察者模式,又称发布-订阅模式。你会发现许多关于如何使用框架中提供的程序集在 .NET Framework 中实现观察者模式的文章。但是,.NET Framework 已经发展了很多年,并且随着时间的推移,它也一直在提供创建观察者模式的新库。
例如,在 .NET 4.0 之前,我们必须编写自定义代码来实现观察者模式,或者使用委托和事件。随着框架 4.0 的发布,出现了非常酷的 IObservable
和 IObserver
接口来实现观察者模式。在本文中,我们将通过代码示例介绍所有这些技术。
背景
为了让大家对观察者模式有一个简要的了解,它定义了对象之间(发布者和多个订阅者)的一对多依赖关系,因此当一个对象(发布者
)改变状态时,所有依赖项(订阅者
)都会自动收到通知并更新。
上述定义深受《Head First Design Patterns》一书的影响。我强烈建议任何开始学习设计模式的人阅读这本书。
示例场景
下面的代码基于一个场景,其中我们有一个气象站,负责记录天气数据(温度、湿度和气压)。
这些数据需要被多个显示器消费并相应地显示。每当气象站有新数据可用时,它应该将数据“推送”给显示器,所有显示器都应相应地更新。假设我们有 3 个显示器(当前状况、统计显示和天气预报显示),当气象站有新数据可用时,所有这些显示器都必须更新。
这个场景与《Head First Design Patterns》一书中提出的场景类似,但在这里,我们将讨论其 .NET 实现。
实现
因此,天气数据对象实际上是一个具有 3 个属性的对象:温度、湿度和气压。
WeatherData
类如下所示:
public class WeatherData
{
public float Temperature { get; set; }
public float Humidity { get; set; }
public float Pressure { get; set; }
public WeatherData(float temp, float humid, float pres)
{
Temperature = temp;
Humidity = humid;
Pressure = pres;
}
}
}
现在,当气象站有新数据可用时,这些数据就需要被推送到天气显示器(订阅者)。
我们这里的代码示例将侧重于实现 .NET 4.0 之前的观察者模式和 .NET 4.0 之后的观察者模式。
每个部分的代码将分为发布者代码(类和接口)和订阅者代码(类和接口)。那么,我们开始吧。
观察者模式(.NET 4.0 之前)
<技术 #1>
使用纯面向对象(OO)编程概念
根据 OO 最佳实践,我们应该始终尝试面向接口编程,而不是面向实现。以下内容基于此。
出版社
这里的发布者
实际上是一个天气数据提供者
,负责提供天气数据。这个WeatherDataProvider
类实现了IPublisher
接口,如下所示:
public interface IPublisher
{
void RegisterSubscriber(ISubscriber subscriber);
void RemoveSubscriber(ISubscriber subscriber);
void NotifySubscribers();
}
上面的三个方法执行以下操作:
RegisterSubscriber
- 向发布者
注册一个新订阅者。发布者必须将订阅者添加到它需要通知的订阅者列表中,以便在WeatherData
发生更改时进行通知。RemoveSubscriber
- 从发布者
的通知列表中删除一个已注册的订阅者
。NotifySubscibers
- 此方法实际上会调用订阅者
对象上的一个方法,以通知其WeatherData
已发生更改。
WeatherDataProvider
的具体实现如下:
public class WeatherDataProvider : IPublisher
{
List<ISubscriber> ListOfSubscribers;
WeatherData data;
public WeatherDataProvider()
{
ListOfSubscribers = new List<ISubscriber>();
}
public void RegisterSubscriber(ISubscriber subscriber)
{
ListOfSubscribers.Add(subscriber);
}
public void RemoveSubscriber(ISubscriber subscriber)
{
ListOfSubscribers.Remove(subscriber);
}
public void NotifySubscribers()
{
foreach (var sub in ListOfSubscribers)
{
sub.Update(data);
}
}
private void MeasurementsChanged()
{
NotifySubscribers();
}
public void SetMeasurements(float temp, float humid, float pres)
{
data = new WeatherData(temp, humid, pres);
MeasurementsChanged();
}
}
订阅者
这里的订阅者实际上是天气显示器,它们消费数据。每个订阅者都应该实现ISubscriber
接口
。
public interface ISubscriber
{
void Update(WeatherData data);
}
Subscriber
接口
只有一个方法;它显示从WeatherDataProvider
接收到的当前WeatherData
。
CurrentConditionsDisplay
的实现如下:
public class CurrentConditionsDisplay : ISubscriber
{
WeatherData data;
IPublisher weatherData;
public CurrentConditionsDisplay(IPublisher weatherDataProvider)
{
weatherData = weatherDataProvider;
weatherData.RegisterSubscriber(this);
}
public void Display()
{
Console.WriteLine("Current Conditions :
Temp = {0}Deg | Humidity = {1}% |
Pressure = {2}bar", data.Temperature, data.Humidity, data.Pressure);
}
public void Update(WeatherData data)
{
this.data = data;
Display();
}
}
在上面的代码中,我们进行了依赖注入或 IoC,通过构造函数注入了IPublisher
接口。构造函数注入是 OO 编程中非常常见的做法。
因此,上面的代码发生的事情是:当显示器被实例化时,它会调用WeatherDataProvider
的RegisterSubscriber
方法,并将自身注册为一个感兴趣的订阅者。
如果显示器想要取消注册,它必须调用WeatherDataProvider
的RemoveSubscriber
方法。有多种方法可以实现这一点:你可以在析构函数
(如上)中调用RemoveSubscriber
,或者实现IDisposable
并在Dispose
方法中调用RemoveSubscriber
,或者只是简单地创建一个类方法并在那里调用。
就像CurrentConditionsDisplay
一样,我们也可以有一个ForecastDisplay
(如下所示):
public class ForecastDisplay : ISubscriber, IDisposable
{
WeatherData data;
IPublisher weatherData;
public ForecastDisplay(IPublisher weatherDataProvider)
{
weatherData = weatherDataProvider;
weatherData.RegisterSubscriber(this);
}
public void Display()
{
Console.WriteLine("Forecast Conditions : Temp = {0}Deg |
Humidity = {1}% | Pressure = {2}bar", data.Temperature + 6,
data.Humidity + 20, data.Pressure - 3);
}
public void Update(WeatherData data)
{
this.data = data;
Display();
}
public void Dispose()
{
weatherData.RemoveSubscriber(this);
}
}
同样,你可以在这里定义任意数量的显示器,它们都可以订阅WeatherStation
以获取更新。
为了演示上面的代码片段,我们将创建一个如下所示的示例控制台应用程序:
class Program
{
static void Main(string[] args)
{
WeatherDataProvider weatherData = new WeatherDataProvider();
CurrentConditionsDisplay currentDisp = new CurrentConditionsDisplay(weatherData);
ForecastDisplay forecastDisp = new ForecastDisplay(weatherData);
weatherData.SetMeasurements(40, 78, 3);
Console.WriteLine();
weatherData.SetMeasurements(45, 79, 4);
Console.WriteLine();
weatherData.SetMeasurements(46, 73, 6);
//Remove forecast display
forecastDisp.Dispose();
Console.WriteLine();
Console.WriteLine("Forecast Display removed");
Console.WriteLine();
weatherData.SetMeasurements(36, 53, 8);
Console.Read();
}
}
输出应如下所示:
<技术 #2>
使用事件和委托
现在,转向在 .NET 中实现观察者模式的第二种方法。第一种方法当然没有利用任何 .NET 库来实现该模式。
在 .NET 中实现观察者模式时,此技术非常常用。它利用 .NET 框架中的事件和委托来实现。
我不会过多深入介绍以下源代码的细节,因为大部分内容都是不言自明的,但如果你需要有关以下实现的更多信息,可以参考 此 MSDN 链接。
首先,此技术利用通用的EventHandler<T>
委托来实现该模式。基本原理非常简单:
- 发布者有一个
public Event
和EventHandler
,它在WeatherData
每次更改时都会触发。 - 订阅者附加到
EventHandler
,以便在事件触发时收到通知。 - 在订阅者处收到的
EventArgs e
包含有关当前天气状况的数据。
首先,我们必须创建一个继承自EventArgs
的WeatherEventArgs
类:
public class WeatherDataEventArgs : EventArgs
{
public WeatherData data { get; private set; }
public WeatherDataEventArgs(WeatherData data)
{
this.data = data;
}
}
此类包含将在事件处理程序中的事件参数中传递的天气数据。
出版社
这里的发布者
是一个名为WeatherDataProvider
的类,它在天气数据更改时触发事件,并通知已订阅的显示器:
public class WeatherDataProvider : IDisposable
{
public event EventHandler<WeatherDataEventArgs> RaiseWeatherDataChangedEvent;
protected virtual void OnRaiseWeatherDataChangedEvent(WeatherDataEventArgs e)
{
// Make a temporary copy of the event to avoid possibility of
// a race condition if the last subscriber unsubscribes
// immediately after the null check and before the event is raised.
EventHandler<WeatherDataEventArgs> handler = RaiseWeatherDataChangedEvent;
if (handler != null)
{
handler(this, e);
}
}
public void NotifyDisplays(float temp, float humid, float pres)
{
OnRaiseWeatherDataChangedEvent
(new WeatherDataEventArgs(new WeatherData(temp, humid, pres)));
}
public void Dispose()
{
if (RaiseWeatherDataChangedEvent != null)
{
foreach (EventHandler<WeatherDataEventArgs>
item in RaiseWeatherDataChangedEvent.GetInvocationList())
{
RaiseWeatherDataChangedEvent -= item;
}
}
}
}
上面的代码声明了一个通用类型的EventHandler
委托,该委托接受WeatherDataEventArgs
作为要提供给事件处理程序的参数。
每次调用NotifyDisplays
方法时,都会触发OnRaiseWeatherDataChangedEvent
,该方法又会在以下代码的相应显示器
(订阅者)中调用事件处理程序。
订阅者
CurrentConditionsDisplay
的实现如下。我们可以有任意数量的显示器附加到Event
,通过Event
处理程序。
public class CurrentConditionsDisplay
{
WeatherData data;
WeatherDataProvider WDprovider;
public CurrentConditionsDisplay(WeatherDataProvider provider)
{
WDprovider = provider;
WDprovider.RaiseWeatherDataChangedEvent += provider_RaiseWeatherDataChangedEvent;
}
void provider_RaiseWeatherDataChangedEvent(object sender, WeatherDataEventArgs e)
{
data = e.data;
UpdateDisplay();
}
public void UpdateDisplay()
{
Console.WriteLine("Current Conditions : Temp = {0}Deg |
Humidity = {1}% | Pressure = {2}bar", data.Temperature, data.Humidity, data.Pressure);
}
public void Unsubscribe()
{
WDprovider.RaiseWeatherDataChangedEvent -= provider_RaiseWeatherDataChangedEvent;
}
}
像上面一样,我们也可以有一个ForecastDisplay
(在源代码中提供)。
可以使用如下代码的控制台应用程序来演示上述用法的示例:
class Program
{
static void Main(string[] args)
{
WeatherDataProvider provider = new WeatherDataProvider();
CurrentConditionsDisplay current = new CurrentConditionsDisplay(provider);
ForecastDisplay forecast = new ForecastDisplay(provider);
provider.NotifyDisplays(40, 78, 3);
Console.WriteLine();
provider.NotifyDisplays(42, 68, 5);
Console.WriteLine();
provider.NotifyDisplays(45, 68, 8);
Console.WriteLine();
forecast.Unsubscribe();
Console.WriteLine("Forecast Display removed");
Console.WriteLine();
provider.NotifyDisplays(30, 58, 1);
//Code to call to detach all event handler
provider.Dispose();
Console.Read();
}
}
观察者模式(.NET 4.0 及更高版本)
当 .NET 框架发布时,它带来了 C# 许多备受期待的语言特性,如动态类型、可选参数、IObservable
等。其中一些特性在其他语言/框架中已经可用,但在 C# 中却不可用。
其中最值得注意的之一是IObservable<T>
和IObserver<T>
。这些在 Java 中就已经可用,直到后来才引入 C#,它们确实是我最喜欢的功能之一。这些集合为我们开发者带来了一些独特而巧妙的功能供我们利用。其中最重要的是能够实现健壮的发布-订阅模型,即观察者模式。
以下实现与 此处 MSDN 链接中的示例实现一致。
<技术 #3>
使用 IObservable<T> 和 IObserver<T>
这些库使得实现观察者模式变得最容易。事实上,它非常简单,以至于在使用这些库时,你可能不会意识到自己正在使用程序中的发布-订阅模型。
在通用类型IObservable<T>
和IObserver<T>
中,我们这里的T
将是WeatherData
。
出版社
发布者(WeatherDataProvider
)只需要实现IObservable<T>
接口(如下所示)。
public class WeatherDataProvider : IObservable<WeatherData>
{
List<IObserver<WeatherData>> observers;
public WeatherDataProvider()
{
observers = new List<IObserver<WeatherData>>();
}
public IDisposable Subscribe(IObserver<WeatherData> observer)
{
if (!observers.Contains(observer))
{
observers.Add(observer);
}
return new UnSubscriber(observers, observer);
}
private class UnSubscriber : IDisposable
{
private List<IObserver<WeatherData>> lstObservers;
private IObserver<WeatherData> observer;
public UnSubscriber(List<IObserver<WeatherData>> ObserversCollection,
IObserver<WeatherData> observer)
{
this.lstObservers = ObserversCollection;
this.observer = observer;
}
public void Dispose()
{
if (this.observer != null)
{
lstObservers.Remove(this.observer);
}
}
}
private void MeasurementsChanged(float temp, float humid, float pres)
{
foreach (var obs in observers)
{
obs.OnNext(new WeatherData(temp, humid, pres));
}
}
public void SetMeasurements(float temp, float humid, float pres)
{
MeasurementsChanged(temp, humid, pres);
}
}
观察者通过调用其IObservable<T>.Subscribe
方法来注册接收通知,该方法将observer
对象的引用分配给一个private
的通用List<T>
对象。该方法返回一个Unsubscriber
对象,它是一个IDisposable
实现,允许观察者停止接收通知。Unsubscriber
类是一个简单的嵌套类,它实现了IDisposable
,并且还维护了一个已订阅用户的列表,并由观察者(在本例中为Displays
)用于取消订阅。我们将在下一节中了解更多关于这方面的信息。
此外,还有一个我们已调用OnNext
函数在订阅者/观察者上。这个函数实际上是IObserver
的内置函数,表示集合中发生了变化。这实际上是通知订阅者变化的函数。
除了OnNext
之外,还有OnError
和OnCompleted
函数。我们将在下一节讨论所有这些。
订阅者
订阅者(Displays
)只需要实现IObserver<T>
接口
。CurrentConditionsDisplay
的实现如下。我们可以类似地有任意数量的显示器(请参见源代码)。
public class CurrentConditionsDisplay : IObserver<WeatherData>
{
WeatherData data;
private IDisposable unsubscriber;
public CurrentConditionsDisplay()
{
}
public CurrentConditionsDisplay(IObservable<WeatherData> provider)
{
unsubscriber = provider.Subscribe(this);
}
public void Display()
{
Console.WriteLine("Current Conditions : Temp = {0}Deg |
Humidity = {1}% | Pressure = {2}bar", data.Temperature, data.Humidity, data.Pressure);
}
public void Subscribe(IObservable<WeatherData> provider)
{
if (unsubscriber == null)
{
unsubscriber = provider.Subscribe(this);
}
}
public void Unsubscribe()
{
unsubscriber.Dispose();
}
public void OnCompleted()
{
Console.WriteLine("Additional temperature data will not be transmitted.");
}
public void OnError(Exception error)
{
Console.WriteLine("Some error has occurred..");
}
public void OnNext(WeatherData value)
{
this.data = value;
Display();
}
}
上面代码中值得关注的点如下:
- 当你调用
subscriber
方法时,它会返回一个实现IDisposable
的对象(在本例中为Unsubscriber
)。因此,当我们调用该对象上的Dispose
时,它会自动调用Unsubscribe
。 - 发布者/提供者可以通过以下 3 种方法调用订阅者/观察者:
IObserver<T>.OnNext
方法,将一个包含当前数据、更改数据或新数据的T
对象传递给观察者。IObserver<T>.OnError
方法,通知观察者发生了某个错误条件(请注意,当提供者中发生异常时,这不会自动调用。程序员的责任是捕获提供者中的异常,然后调用此函数)。IObserver<T>.OnCompleted
方法,通知观察者它已完成发送通知。
可以使用如下代码的控制台应用程序来创建演示此代码的演示程序:
class Program
{
static void Main(string[] args)
{
WeatherDataProvider weatherDataO = new WeatherDataProvider();
CurrentConditionsDisplay currentDisp = new CurrentConditionsDisplay(weatherDataO);
ForecastDisplay forecastDisp = new ForecastDisplay(weatherDataO);
weatherDataO.SetMeasurements(40, 78, 3);
Console.WriteLine();
weatherDataO.SetMeasurements(45, 79, 4);
Console.WriteLine();
weatherDataO.SetMeasurements(46, 73, 6);
//Remove forecast display
forecastDisp.Unsubscribe();
Console.WriteLine();
Console.WriteLine("Forecast Display removed");
Console.WriteLine();
weatherDataO.SetMeasurements(36, 53, 8);
Console.Read();
}
}
关注点
当我开始学习设计模式,特别是观察者模式时,我发现可以在 .NET Framework 中以多种方式实现它。
经过一番研究,我平息了我的好奇心,并确定了上述 3 种实现技术。当然,你可能还有其他很棒的观察者模式实现方式,我很乐意了解。请在下方的评论中告诉我。
历史
- 第一版
- 代码更改建议由 Paulo Zemek
- 代码更改建议由 CatchExAs