在 .NET 中实现 Observer

来源:互联网 发布:闲鱼淘宝小二介入几天 编辑:程序博客网 时间:2024/05/01 00:42

在 .NET 中实现 Observer

发布日期: 4/1/2004 | 更新日期: 4/16/2004

使用 Microsoft .NET 的企业解决方案模式 > Web 表示模式 > 在 .NET 中实现 Observer

版本: 1.0.1

本页内容
上下文 上下文 背景信息 背景信息 实现策略 实现策略 测试考虑事项 测试考虑事项 结果上下文 结果上下文 相关模式 相关模式 致谢 致谢

上下文

您要在 Microsoft? .NET 中构建一个应用程序,并且必须在不使源对象依赖于依赖性对象的情况下将状态更改通知该依赖性对象。

返回页首返回页首

背景信息

为了解释如何在 .NET 中实现 Observer (观察器),并说明限制对象之间的依赖性所获得的好处,下面的示例重构了一个具有双向依赖关系的解决方案。首先,将该解决方案重构为基于 Design Patterns [Gamma95] 中所定义的 Observer 模式的实现;然后,利用对实现有单一继承性的语言,将该解决方案重构为 Observer 模式的修改形式;最后,重构为使用 .NET Framework 语言的委派和事件构造的解决方案。

该示例问题有两个类,AlbumBillingService(请参阅图 1)。


1 UML 静态图示例

这两个对象通过交互来显示相册内容,并在每次显示相册内容时向最终用户收费。

Album.cs

下面的示例显示了 Album 类的实现:

using System; public class Album  {    private BillingService billing;    private String name;     public Album(BillingService billing,              string name)    {        this.billing = billing;       this.name = name;     }    public void Play()     {       billing.GenerateCharge(this);       // code to play the album    }    public String Name    {       get { return name; }    } } 

BillingService.cs

下面的示例显示了 BillingService 类的实现:

using System; public class BillingService  {    public void GenerateCharge(Album album)     {       string name = album.Name;       // code to generate charge for correct album    } } 

这些类必须按特定顺序创建。因为构造 Album 类时需要 BillingService 对象,所以前者必须在后者之后构造。对象实例化之后,每次调用 Play 方法时就会向用户收费。

Client.cs

下面的 Client 类演示了构造过程:

using System; class Client {    [STAThread]    static void Main(string[] args)    {       BillingService service = new BillingService();       Album album = new Album(service, "Up");       album.Play();    } } 

此代码运行正常,但至少有两个问题第一个问题Album 类和 BillingService 类之间的双向依赖性。Album 调用了 BillingService 的方法,而 BillingService 也调用了 Album 的方法。这意味着,如果您需要在其他地方重用 Album 类,那么还必须同时包括 BillingService。同样,您也不能在没有 Album 类的情况下使用 BillingService 类。这种情况是不理想的,因为它限制了灵活性。(这里依赖的定义就是:在一个类中引用了另一个类的对象。如果没有另一个类,或者在生成过程中如果另一个类没有先实现的话,那么这个类就不能正常的工作,这就是依赖)

第二个问题是,您必须在每次添加或删除新的服务时修改 Album 类。例如,如果要添加一个用于跟踪相册显示次数的计数器服务,则必须按以下方式修改 Album 类的构造函数和 Play 方法:

using System; public class Album  {    private BillingService billing;    private CounterService counter;    private String name;     public Album(BillingService billing,          CounterService counter,              string name)    {        this.billing = billing;       this.counter = counter;       this.name = name;     }    public void Play()     {       billing.GenerateCharge(this);       counter.Increment(this);       // code to play the album    }    public String Name    {       get { return name; }    } } 

这种做法非常不好。显然,这种类型的更改根本不应该涉及 Album 类。此设计使代码难以维护。但是,您可以使用 Observer 模式来解决这些问题。

返回页首返回页首

实现策略

针对上一部分所描述的问题,此策略讨论和实现了许多方法。每个解决方案都试图通过取消 AlbumBillingService 之间的双向依赖性,来纠正前面示例中的问题。第一个解决方案描述了如何通过使用 Design Patterns [Gamma95] 中所描述的解决方案来实现 Observer 模式。

观察器

Design Patterns 方法使用抽象的 Subject 类和 Observer 接口来取消 Subject 对象和 Observer 对象之间的依赖性。它还考虑到一个 Subject 可以有多个 Observer。在本示例中,Album 类从 Subject 类继承而来,因此承担了 Observer 模式中所描述的具体主体的角色。BillingService 类通过实现 Observer 接口代替了具体观察器,因为当 Play 方法被调用时 BillingService 正在等待接收通知。(请参阅图 2。)


2 Observer 类图

通过扩展 Subject 类,可以取消 Album 类对 BillingService 的直接依赖性。但是,您现在对 Observer 接口有依赖性。因为 Observer 是一个接口,所以系统不依赖于实现接口的实际实例。因此,不必修改 Album 类就可以轻松地进行扩展。您仍然没有取消 BillingServiceAlbum 之间的依赖性。这不能算是很大的问题,因为您可以很容易地添加新的服务,而不必更改 Album。下面的示例显示了此解决方案的实现代码。(?)

Observer.cs

下面的示例显示了 Observer 类:

using System; public interface Observer {    void Update(object subject); } 

Subject.cs

下面的示例显示了 Subject 类:

using System; using System.Collections; public abstract class Subject {    private ArrayList observers = new ArrayList();     public void AddObserver(Observer observer)    {       observers.Add(observer);    }    public void RemoveObserver(Observer observer)    {       observers.Remove(observer);    }    public void Notify()    {       foreach(Observer observer in observers)       {          observer.Update(this);       }    } } 

Album.cs

下面的示例显示了 Album 类:

using System; public class Album : Subject {    private String name;     public Album(String name)    { this.name = name; }    public void Play()     {       Notify();       // code to play the album    }    public String Name    {       get { return name; }    } } 

BillingService.cs

下面的示例显示了 BillingService 类:

using System; public class BillingService : Observer {    public void Update(object subject)    {       if(subject is Album)          GenerateCharge((Album)subject);    }    private void GenerateCharge(Album album)     {       string name = album.Name;       //code to generate charge for correct album    } } 

您可以在该示例中验证 Album 类不再依赖于 BillingService 类。如果您需要在其他上下文中使用 Album 类,这已经是很理想的了。在“背景信息”的示例中,如果要使用 Album,需要同时包括 BillingService 类。

Client.cs

下面的代码描述了如何创建各种对象以及创建对象的顺序。此构造代码和“背景信息”示例之间的最大区别是 Album 类获得 BillingService 的相关信息的方式。在“背景信息”示例中,BillingService 作为构造参数显式地传递到 Album。在此示例中,则调用名为 AddObserver 的函数来添加实现了 Observer 接口的 BillingService

using System; class Client {    [STAThread]    static void Main(string[] args)    {       BillingService billing = new BillingService();       Album album = new Album("Up");       album.AddObserver(billing);       album.Play();    } } 

如您所见,Album 类没有引用计费服务。它必须做的所有工作就是继承 Subject 类。Client 类将对 BillingService 的实例的引用传递给相册,但语言运行库自动地将 BillingService 引用转换为对 Observer 接口的引用。AddObserver 方法(在 Subject 基类中实现)只处理对 Observer 接口的引用;它也不会引用计费服务。因此,这样就取消了 Album 类对任何与计费服务有关的内容的依赖性。不过,这仍然存在许多问题:

使用继承来共享 Subject 实现。Microsoft Visual Basic? .NET 开发系统和 C# 语言允许实现的单一继承和接口的多重继承。在此示例中,您需要使用单一继承来共享 Subject 实现。这样就无法使用它在继承层次结构中对 Albums 分类。

单个可观察活动。在任何时候,只要调用了 Play 方法,Album 类就会通知观察器。如果您有另一个函数(例如 Cancel),则必须将该事件与 Album 对象一起发送给服务,这样服务才能知道这是 Play 事件还是 Cancel 事件。这就使服务变得很复杂,因为服务会收到它们可能不感兴趣的事件。

降低了显式程度,提高了复杂性。现在取消了直接依赖性,但代码的显式程度降低了。在原来的实现中,AlbumBillingService 有直接依赖性,因此,可以很容易地知道调用 GenerateCharge 方法的方式和时间。而在此示例中,Album 调用 Subject 中的 Notify 方法,该方法则通过一列以前注册的 Observer 对象进行迭代,然后再调用 Update 方法。在 BillingService 类中,Update 方法将调用 GenerateCharge。如果您想详细了解显式方式的优点,请参阅 Martin Fowler IEEE Software [Fowler01] 中的文章“To Be Explicit”。

修改后的 Observer

Observer [Gamma95] 的主要缺点是,使用继承作为共享 Subject 实现的方法。另外,这样就无法显式地知道 Observer 对收到哪些活动的通知感兴趣。为了解决这些问题,该示例的下一个部分引入了经过修改的 Observer。在此解决方案中,您将 Subject 类改为一个接口。您还引入了名为 SubjectHelper 的另一个类,它实现了 Subject 接口(请参阅图 3)。


3 经过修改的 Observe类图

Album 类包含 SubjectHelper,并将它作为公用属性公开。这就允许像 BillingService 这样的类访问特定的 SubjectHelper,并指出如果 Album 类发生更改它希望得到通知。此实现还允许 Album 类有一个以上的 SubjectHelper;也许,每个公开的活动各有一个。下面的代码实现了此解决方案(这里省略了 Observer 接口和 BillingService 类,因为它们没有变化)。

Subject.cs

在下面的示例中,Notify 已经更改,因为现在您必须将 Subject 传递给 SubjectHelper 类。这在 Observer [Gamma95] 示例中是非必要的,因为 Subject 类是基类。

using System; using System.Collections; public interface Subject {    void AddObserver(Observer observer);    void RemoveObserver(Observer observer);    void Notify(object realSubject); } 

SubjectHelper.cs

下面的示例显示了新创建的 SubjectHelper 类:

using System; using System.Collections; public class SubjectHelper : Subject {    private ArrayList observers = new ArrayList();     public void AddObserver(Observer observer)    {       observers.Add(observer);    }    public void RemoveObserver(Observer observer)    {       observers.Remove(observer);    }    public void Notify(object realSubject)    {       foreach(Observer observer in observers)       {          observer.Update(realSubject);       }    } } 

Album.cs

下面的示例显示,当使用 SubjectHelper 而不是继承 Subject 类时,Album 类有哪些更改:

using System; public class Album {    private String name;     private Subject playSubject = new SubjectHelper();    public Album(String name)    { this.name = name; }    public void Play()     {       playSubject.Notify(this);       // code to play the album    }    public String Name    {       get { return name; }    }    public Subject PlaySubject    {       get { return playSubject; }    } } 

Client.cs

下面的示例显示了 Client 类有哪些更改:

using System; class Client {    [STAThread]    static void Main(string[] args)    {       BillingService billing = new BillingService();       CounterService counter = new CounterService();       Album album = new Album("Up");       album.PlaySubject.AddObserver(billing);       album.PlaySubject.AddObserver(counter);       album.Play();    } } 

也许,您已经可以看到减少类之间的耦合所带来的某些优点。例如,虽然这种重构调整了 SubjectAlbum 的实现,但 BillingService 类根本不必更改。另外,Client 类现在更易于阅读,因为您可以指定要将服务连接到哪个具体的事件。

显然,修改后的 Observer 解决方案解决了以前的解决方案存在的问题。实际上,对于只有单一实现继承的语言来说,这是首选的实现方法。不过,此解决方案仍然有以下缺点:

更加复杂。原来的解决方案由两个类组成,它们以显式方式直接相互引用,而现在的解决方案则有两个接口和三个类进行间接对话,并且还包括第一个示例中所没有的许多代码。毫无疑问,您开始考虑,原来的依赖性是否没有那么糟糕。不过,您应该记住,这两个接口和 SubjectHelper 类可以由任意多个观察器重用。因此,在整个应用程序中,它们可能只需要编写一次。

降低了显式性。此解决方案与 Observer [Gamma95] 一样,很难确定哪个观察器在观察 Subject 的更改。

因此,此解决方案是很好的面向对象设计,但需要您创建许多类、接口、关联等等。所有这一切在 .NET 中确实有必要吗?回答是“否”,请看下面示例。

.NET 中的观察器

使用 .NET 的内置功能,您只需少得多的代码就可以实现 Observer 模式。您不需要 SubjectSubjectHelperObserver 类型,因为有了公共语言运行库,它们就已经过时了。在 .NET 中引入的委派和事件使您不必开发特定类型就能实现 Observer

在基于 .NET 的实现中,事件代表了一种在“修改后的观察器”中所描述的 SubjectHelper 类的抽象(受公共语言运行库和各种编译器支持)。Album 类公开事件类型,而不是 SubjectHelper。观察器角色比以前要稍微复杂一些。观察器必须创建特定的委派实例,并向主体事件注册该委派,而不是实现 Observer 接口并向主体注册自身。观察器必须使用由事件声明所指定的类型的委派实例;否则,注册将失败。在此委派实例的创建期间,观察器提供将接受主体通知的方法名(实例或静态)。当委派绑定到方法之后,它就可以向主体的事件进行注册。同样,也可以从事件注销此委派。主体通过调用事件向观察器提供通知。 [Purdy02]

下面的代码示例演示了为了使用委派和事件而必须对“修改后的观察器”中的示例所做的更改。

Album.cs

下面的示例显示了 Album 类如何公开事件类型:

using System; public class Album  {    private String name;     public delegate void PlayHandler(object sender);    public event PlayHandler PlayEvent;    public Album(String name)    { this.name = name; }    public void Play()     {       Notify();       // code to play the album    }    private void Notify()    {       if(PlayEvent != null)           PlayEvent(this);    }    public String Name    {       get { return name; }    } } 

BillingService.cs

如以下示例所示,对“修改后的观察器”中的示例内的 BillingService 类的更改只需要删除 Observer 接口的实现:

using System; public class BillingService {    public void Update(object subject)    {       if(subject is Album)          GenerateCharge((Album)subject);    }    private void GenerateCharge(Album theAlbum)     {       //code to generate charge for correct album    } } 

Client.cs

下面的示例显示了如何修改 Client 类,以使用由 Album 类公开的新事件:

using System; class Client {    [STAThread]    static void Main(string[] args)    {       BillingService billing = new BillingService();       Album album = new Album("Up");       album.PlayEvent += new Album.PlayHandler(billing.Update);       album.Play();    } } 

正如您看到的那样,该程序的结构与前面的示例非常类似。.NET 的内置功能取代了显式的 Observer 机制。当您习惯了委派和事件的语法后,它们的使用就显得更为直观。您不必实现“修改后的观察器”中描述的 SubjectHelper 类以及 SubjectObserver 接口。这些概念直接在公共语言运行库中实现。

委派的最大优点是它们能够引用任何方法(只要该方法符合相同的签名)。这就允许任何类充当观察器,无论它实现什么接口或者继承什么类。使用 ObserverSubject 接口减少了观察器类和主体类之间的耦合,而委派的使用则完全取消了这种耦合。有关此主题的详细信息,请参阅 MSDN? 开发人员程序库中的“Exploring the Observer Design Pattern”主题 [Purdy02]。

返回页首返回页首

测试考虑事项

因为委派和事件完全取消了 Album 和 BillingService 之间的双向依赖性,您现在可以独立编写这两个类的测试代码。

AlbumFixture.cs

AlbumFixture 类描述了 NUnit (http://www.nunit.org) 中的示例单元测试,这些测试验证了在调用 Play 方法时是否触发 PlayEvent:

using System; using NUnit.Framework; [TestFixture] public class AlbumFixture {    private bool eventFired;     private Album album;    [SetUp]    public void Init()    {       album = new Album("Up");       eventFired = false;     }    [Test]    public void Attach()    {       album.PlayEvent += new Album.PlayHandler(OnPlay);       album.Play();       Assertion.AssertEquals(true, eventFired);    }    [Test]    public void DoNotAttach()    {       album.Play();       Assertion.AssertEquals(false, eventFired);    }    private void OnPlay(object subject)    {       eventFired = true;    } } 
返回页首返回页首

 

结果上下文

进行综合衡量后,使用委派和事件模型在 .NET 中实现 Observer 所带来的优点显然超过了潜在的缺点。

优点

在 .NET 中实现 Observer 有以下优点

取消了依赖性。上述示例清楚地显示出 AlbumBillingService 类之间的依赖性已经取消。

提高了可扩展性。“.NET 中的观察器”示例说明了添加新类型的观察器是多么简单。Album 类是“开-闭”原则的一个例子,最初是 Bertrand MeyerObject-Oriented Software Construction 第二版 [Bertrand00] 中编写的,它描述了一个易于扩充但不必修改的类。Album 类体现了这种原则,因为您可以添加 PlayEvent 的观察器,而不必修改 Album 类。

提高了可测试性。“测试考虑事项”说明了您为什么可以不必对 BillingService 进行实例化就能测试 Album 类。测试验证了 Album 类能正确运行。测试还提供了如何编写 BillingService 的出色示例。

缺点

如示例所示,Observer 的实现简单而直接。不过,随着委派和事件的数目不断增加,我们很难跟踪当事件触发时发生了什么情况。因此,代码变得很难调试,因为您必须在代码中搜索观察器。

返回页首返回页首

相关模式

有关这里讨论的概念的详细背景,请参阅以下相关模式:

Observer

Model-View-Controller

返回页首返回页首

致谢

[Bertrand00] Meyer, Bertrand. Object-Oriented Software Construction, 2nd Edition. Prentice-Hall, 2000.

[Fowler01] Fowler, Martin. "To Be Explicit." IEEE Software, November/December 2001.

[Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.

[Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.

[Purdy02] Purdy, Doug; Richter, Jeffrey. "Exploring the Observer Design Pattern." MSDN Library, January 2002. Available at: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/observerpattern.asp.