为了解释如何在 .NET 中实现 Observer (观察器),并说明限制对象之间的依赖性所获得的好处,下面的示例重构了一个具有双向依赖关系的解决方案。首先,将该解决方案重构为基于 Design Patterns [Gamma95] 中所定义的 Observer 模式的实现;然后,利用对实现有单一继承性的语言,将该解决方案重构为 Observer 模式的修改形式;最后,重构为使用 .NET Framework 语言的委派和事件构造的解决方案。
该示例问题有两个类,Album 和 BillingService(请参阅图 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 模式来解决这些问题。 实现策略
针对上一部分所描述的问题,此策略讨论和实现了许多方法。每个解决方案都试图通过取消 Album 和 BillingService 之间的双向依赖性,来纠正前面示例中的问题。第一个解决方案描述了如何通过使用 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 类就可以轻松地进行扩展。您仍然没有取消 BillingService 和 Album 之间的依赖性。这不能算是很大的问题,因为您可以很容易地添加新的服务,而不必更改 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 类对任何与计费服务有关的内容的依赖性。不过,这仍然存在许多问题:
修改后的 ObserverObserver [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(); } } 也许,您已经可以看到减少类之间的耦合所带来的某些优点。例如,虽然这种重构调整了 Subject 和 Album 的实现,但 BillingService 类根本不必更改。另外,Client 类现在更易于阅读,因为您可以指定要将服务连接到哪个具体的事件。 显然,修改后的 Observer 解决方案解决了以前的解决方案存在的问题。实际上,对于只有单一实现继承的语言来说,这是首选的实现方法。不过,此解决方案仍然有以下缺点:
因此,此解决方案是很好的面向对象设计,但需要您创建许多类、接口、关联等等。所有这一切在 .NET 中确实有必要吗?回答是“否”,请看下面示例。 .NET 中的观察器使用 .NET 的内置功能,您只需少得多的代码就可以实现 Observer 模式。您不需要 Subject、SubjectHelper 和 Observer 类型,因为有了公共语言运行库,它们就已经过时了。在 .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 类以及 Subject 和 Observer 接口。这些概念直接在公共语言运行库中实现。 委派的最大优点是它们能够引用任何方法(只要该方法符合相同的签名)。这就允许任何类充当观察器,无论它实现什么接口或者继承什么类。使用 Observer 和 Subject 接口减少了观察器类和主体类之间的耦合,而委派的使用则完全取消了这种耦合。有关此主题的详细信息,请参阅 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 有以下优点:
缺点 如示例所示,Observer 的实现简单而直接。不过,随着委派和事件的数目不断增加,我们很难跟踪当事件触发时发生了什么情况。因此,代码变得很难调试,因为您必须在代码中搜索观察器。 |