在 C# 中实现观察者模式






4.92/5 (6投票s)
目标是尽可能简单地在 C# 中实现观察者模式,并利用框架的优势。
引言
观察者模式可以说是 GOF 设计模式书中1 最有趣的、如果不是最重要的设计模式。
该模式的目的是“定义对象之间的一对多依赖关系,以便当一个对象的状态发生变化时,其所有依赖项都会自动收到通知和更新”2。
本文的目标是将此定义映射到 C#.net 世界,即提供一个尽可能简单且能利用框架的实现。
我读过一些涉及此主题的文章。但我感觉它们要么未能完全涵盖(请参阅下面的参与者);要么试图在不使用可用 C# 构造的情况下重新实现,或者未能引用参考文献。
这也是一个自我备忘录。希望在我写作的过程中能精炼我的理解;并从您的评论中扩展它。
1[Gamma1995]2[Gamma1995], p.326
背景
摘自 GOF 设计模式书3
![]()
主题观察者
- 知道它的观察者。任何数量的观察者对象都可以观察一个主题。
- 提供一个接口,用于附加和分离观察者对象。
具体主题
- 定义一个更新接口,用于应该被通知主题更改的对象。
具体观察者
- 存储 ConcreteObserver 对象感兴趣的状态。
- 当状态发生变化时,向其观察者发送通知。
- 维护对 ConcreteSubject 对象的引用。
- 存储应与主题保持一致的状态。
- 实现 Observer 更新接口,以使其状态与主题保持一致。
我从书中引用这句话,因为它简洁地定义了模式是什么以及参与者是谁。
请注意,书中还讨论了如何实现该模式4,并提供了 C++ 的示例代码5。我建议阅读这本书,因为其中的思想非常有用且永恒。
3[Gamma1995],Structure and Participants, p.3284[Gamma1995], p.330
5[Gamma1995], p.334
委托和事件
在 .Net 中实现观察者模式的最简单方法是使用委托和事件。
微软将引发事件的类称为发布者(publisher),将处理事件的类称为订阅者(subscriber)6。值得注意的是,在 GOF 书中它也被称为“发布-订阅”模式7。
我将尝试按照我通常编写代码的顺序来定义这些组件。我觉得这在重用模式时很有用。请原谅我过多地使用项目符号,但我发现它们在表达观点时很有用。
6[Events]7[Gamma1995], p.326
委托
- 委托(delegate)是一种定义方法签名的类型8。我认为它是一种强大的构造,允许我们定义一个“接口”,即交互点9,而无需紧密耦合。
- 所有 .Net Framework 类库事件都基于
System EventHandler
委托或泛型EventHandler<TEventArgs>
10。以下代码显示了 System EventHandler 的声明。
namespace System { public delegate void EventHandler(object sender, EventArgs e); }
- 您可以按任何您想要的方式定义委托,但我建议使用
System EventHandler
或泛型版本。此外,请遵循事件发布指南11。 - 委托映射到模式中的观察者。它定义了通知观察者的方法接口,即方法签名。在 .Net 中,您甚至不需要定义自己的 Observer。您只需要使用
EventHandler
或其泛型版本。
9[Interface]
10[Guidelines]
11[Guidelines]
事件
- 事件(event)是一个关键字,用作委托的修饰符,用于在类中声明一个“事件”12。“事件”是某个有趣的事情,例如状态更改或方法被调用。
- 事件是一个多播委托(multicast delegate)。它允许多个具有匹配签名的方法;不一定完全匹配13,进行订阅。
- 以下代码显示了如何声明一个基于 System
EventHandler
的事件。
public class AClass { public event EventHandler MyEvent; }
- 编译器会自动添加事件访问器,add 和 remove14。这使得上面的代码在没有锁定机制的情况下等同于以下代码
private EventHandler myEvent; public event EventHandler MyEvent { add { click = (EventHandler)Delegate.Combine(click, value); } remove { click = (EventHandler)Delegate.Remove(click, value); } }
- 这显示了如何附加和分离事件处理程序(Event Handlers),即观察者。
- 要通知观察者,您只需调用事件。但以下是更健壮的实现,遵循 MS 的指南
protected virtual void OnAPropertyChanged(EventArgs 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 handler = MyEvent; // Event will be null if there are no subscribers if (handler != null) { // Use the () operator to raise the event. handler(this, e); } }
- 请注意,事件最初为 null,因此在调用之前需要检查是否有订阅者。我们调用该副本以确保在使用它之前它不会变为 null,即实现线程安全15。
- 事件映射到主题(Subject)。它知道它的观察者,即订阅者。它允许任意数量的观察者,即它是一个多播委托。它提供了一个附加和分离的接口,即 add 和 remove。
13[Variance]
14[Custom]
15[Thread]
具体主题
- ConcreteSubject 存储感兴趣的状态并在状态更改时发送通知。这很好地映射到任何具有状态(即字段)并为该字段或其访问器(属性)声明事件的类。
- 唯一需要注意的是 GOF 的图16中 ConcreteSubject 继承自 Subject。我们可以将事件包装在可继承类型中,例如接口,或者就这样让它存在。
- 包装事件的一个示例是
System
ComponentModel
IComponent
namespace System.ComponentModel { public interface IComponent : IDisposable { ... // Summary: // Represents the method that handles the // System.ComponentModel.IComponent.Disposed // event of a component. event EventHandler Disposed; } }
- MS 的实现中我感到奇怪的一点是状态(例如
IsDisposed
)定义在System.Windows.Forms.Control
中。我猜奇怪的是事件位于IComponent
下而不是IDisposable
。我同意组合优于继承17,但将与IDisposable
相关的事件、属性等放在其下会更具可读性和可重用性吗? - 我扯远了。重点是您可以将事件包装在接口或基类中,以便进行类型继承。但我认为这是不必要的。我的观点是组合,虽然不严格属于类型继承,但同样好,如果不是更好的话。如前所述,委托的行为类似于接口,类似于基于原型的继承模型18。因此,仅通过声明事件,您就已经继承了委托的所有优点。在我们之前的示例中,
AClass
是我们的 ConcreteSubject。唯一缺少的是感兴趣的状态,我们将使用字段和属性来实现它。我们还将钩子属性设置器到事件,以便我们获得自动通知。请确保在实际更改状态时检查设置器值。这将为您节省大量不必要的通知。
public class AClass { private int stateOfInterest; public event EventHandler StateOfInterestChanged; public int StateOfInterest { get { return this.stateOfInterest; } set { // Check if the new value is different // from the current to prevent unnecessary // triggers if (this.stateOfInterest != value) { this.stateOfInterest = value; this.OnStateOfInterestChanged(EventArgs.Empty); } } } protected virtual void OnStateOfInterestChanged(EventArgs 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 handler = this.StateOfInterestChanged; // Event will be null if there are no subscribers if (handler != null) { // Use the () operator to raise the event. handler(this, e); } } }
17[Composition]
18[Differential]
具体观察者
- 这样我们就剩下 ConcreteObserver。在 .Net 世界中,它只是订阅事件的类。有很多订阅事件的方法19,但尽量使用编程方法,这样您可以拥有更多控制权。
public class BClass { AClass aclass; public BClass() { this.aclass = new AClass(); this.aclass.StateOfInterestChanged += this.AClass_StateOfInterestChanged; } void AClass_StateOfInterestChanged(object sender, EventArgs e) { throw new NotImplementedException(); // or do something meaningful } }
- 请记住,在正确实现时,您可以将发送者(sender)强制转换为
AClass
。因此,即使您有多个AClass
,例如一个AClass
的集合,您也可以使用相同的处理程序。
public class CClass { private ObservableCollection<aclass> listOfAClass; public CClass() { this.listOfAClass = new ObservableCollection<aclass>(); this.listOfAClass.CollectionChanged += new NotifyCollectionChangedEventHandler( this.ListOfAClass_CollectionChanged); } private void ListOfAClass_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e) { // we will just use a blanket approach to // hooking up added or removed objects e.OldItems.Cast<aclass>() .ToList<aclass>() .ForEach( a => a.StateOfInterestChanged -= this.AClass_StateOfInterestChanged); e.NewItems.Cast<aclass>() .ToList<aclass>() .ForEach( a => a.StateOfInterestChanged += this.AClass_StateOfInterestChanged); } private void AClass_StateOfInterestChanged( object sender, EventArgs e) { throw new NotImplementedException(); // or do something meaningful } }
摘要
我的目标是使用简单的实现将观察者模式映射到 .Net 世界。我们通过简单地使用委托和事件实现了这一点。
我们进行了一些微调,例如在触发事件之前检查属性值是否实际发生更改,或者按照 MS 指南所述拥有一个 OnXXXChanged()
方法。但总的来说,实现起来很简单。
一个更大的问题可能是“我该如何应用它?”。控件是该模式的出色示例;无论是 WinForm 还是 Web。对于简单的情况,您可以将控件视为主题,将窗体或容器视为观察者,例如 CheckBox
有一个 CheckState
和一个 CheckedStateChanged
事件,所以一个人可以很容易地看到这如何映射到 ConcreteSubject、State of Interest 和 Subject。
但是,当控件作为观察者使用复杂的绑定实现时,它们会更强大;而这,我认为,是另一篇文章的主题。
历史
- 20120911 - 原始版本
参考文献
[Gamma1995]; Gamma, E., R. Helm, R. Johnson, and J. Vlissides; 1995; Design Patterns: Elements of Reusable Object-Oriented Software; Addison Wesley Professional.
[Delegates]; Delegates (C# Programming Guide); http://msdn.microsoft.com/en-us/library/ms173171(v=vs.100).aspx
[Events]; Events (C# Programming Guide); http://msdn.microsoft.com/en-us/library/awbftdfh(v=vs.100).aspx
[Guidelines]; How to: Publish Events that Conform to .NET Framework Guidelines (C# Programming Guide); http://msdn.microsoft.com/en-us/library/w369ty8x(v=vs.100).aspx
[event]; event (C# Reference); http://msdn.microsoft.com/en-us/library/8627sbea(v=vs.100)
[Custom]; How to: Implement Custom Event Accessors (C# Programming Guide); http://msdn.microsoft.com/en-us/library/bb882534(v=vs.100).aspx
[Interface]; Interface (computing); http://en.wikipedia.org/wiki/Interface_(computing)
[Variance]; Using Variance in Delegates (C# and Visual Basic) http://msdn.microsoft.com/en-us/library/ms173174(v=vs.100).aspx
[Thread]; Thread safety; http://en.wikipedia.org/wiki/Thread_safety
[OMT]; Object-modeling technique; http://en.wikipedia.org/wiki/Object-modeling_technique
[Composition]; Composition over inheritance; http://en.wikipedia.org/wiki/Composition_over_inheritance
[Differential]; Differential inheritance; http://en.wikipedia.org/wiki/Differential_inheritance
[Subscribe] How to: Subscribe to and Unsubscribe from Events (C# Programming Guide); http://msdn.microsoft.com/en-us/library/ms366768(v=vs.100).aspx