65.9K
CodeProject 正在变化。 阅读更多。
Home

在 C# 中实现观察者模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (6投票s)

2012年9月12日

CPOL

7分钟阅读

viewsIcon

48083

目标是尽可能简单地在 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.328
4[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 或其泛型版本。
8[Delegates]
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。
12[event]
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);
            }
        }
    }
    
16[OMT]
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
        }
    }
    
19[Subscribe]

摘要

我的目标是使用简单的实现将观察者模式映射到 .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 

© . All rights reserved.