在面向对象的编程中,对象同时包含数据和行为,这两者一起表示业务域的特定方面。使用对象生成应用程序的优点之一是可以将所有数据操作封装在对象内。这样, 就使对象成为独立的单位,并增加了在其他应用程序中重用对象的可能性。但是,对象无法在孤立状态下工作。在除最不重要的应用程序之外的所有应用程序中,对象必须协作才能完成更复杂的任务。当对象协作时,对象可能必须在对象状态发生更改时互相通知对方。例如,Model-View-Controller 模式规定将业务数据(模型)与显示逻辑(视图)分离。当模型发生改变时,系统必须通知视图以便它可以刷新可视显示,并准确地反映模型的状态。换句话说,视图依赖于模型,以便获知模型内部状态的更改。
一个对象如何将状态更改通知其他对象,而又不依赖于这些对象的类?
要解决此问题,就必须协调下列影响因素和注意事项:
将状态更改通知依赖对象的最容易的方法是直接调用它们。但是,对象之间的直接协作需要让它们的类建立相互的依赖性。例如,如果模型对象调用视图对象以便将更改通知它,则模型类现在也会依赖于视图类。两个对象之间的这种直接耦合(也称为紧耦合)降低了类的可重用性。例如,每当要重用模型类时,还必须重用视图类,因为模型会调用它。如果有多个视图,问题就更复杂了。
在事件驱动的框架中,经常出现需要耦合类的情况。框架必须能够将事件通知应用程序,但是框架不能依赖于特定应用程序类。
同样,如果对视图类进行了更改,则模型很可能受到影响。包含许多紧耦合类的应用程序往往是脆弱的和难于维护的,因为一个类中的更改可能影响所有的紧耦合类。
如果直接调用依赖对象,则在每次添加新依赖项时,都必须对源对象内的代码进行修改。
在某些情况下,依赖性对象的数目在设计时可能是未知的。例如,如果您允许用户打开某个特定模型的多个窗口(视图),则必须在模型状态改变时更新多个视图。
直接的函数调用仍然是在两个对象之间传递信息时效率最高的方式(仅次于让两个对象的功能同时包含在同一个对象中)。因此,使用其他机制将对象功能分隔开来很可能对性能有负面影响。取决于应用程序的性能要求,您可能必须对此进行权衡。
解决方案
使用 Observer 模式在独立的对象(主体)中维护一个对主体感兴趣的依赖项(观察器)列表。让所有观察器各自实现公共的 Observer 接口,以取消主体和依赖性对象之间的直接依赖关系(见图 1)。
同样,如果对视图类进行了更改,则模型很可能受到影响。包含许多紧耦合类的应用程序往往是脆弱的和难于维护的,因为一个类中的更改可能影响所有的紧耦合类。
图 1:基本的 Observer 结构
在与依赖性对象相关的客户端中发生状态更改时,ConcreteSubject 会调用 Notify() 方法。Subject 超类用于维护所有感兴趣观察器组成的列表,以便让 Notify() 方法能够遍历所有观察器列表,并调用每个已注册的观察器上的 Update() 方法。观察器通过调用 Subject 上的 subscribe() 和 unsubscribe() 方法,来注册到更新和取消注册(见图 2)。ConcreteObserver 的一个或多个实例可能也会访问 ConcreteSubject 以获取详细信息,因此通常依赖于 ConcreteSubject 类。但是,如图 1 所示,ConcreteSubject 类既不直接也不间接依赖于 ConcreteObserver 类。
图 2:基本的 Observer 交互
使用在主体和观察器之间进行通信这种普通方法,可以动态而不是静态地构建协作。由于将通知逻辑与同步逻辑分离,因此可以添加新观察器而不必修改通知逻辑,而且也可以更改通知逻辑而不会影响观察器中的同步逻辑。代码现在的分离程度更高,因此更易于维护和重用。
将更改通知对象而不致于依赖这些对象的类是一项很常见的要求,因此,某些平台提供了语言支持来执行此功能。例如,Microsoft® .NET Framework 定义了委托和事件这两个概念以实现 Observer 角色。因此,很少需要在 .NET 中显式地实现 Observer 模式,而应该使用委托和事件。大多数 .NET 开发人员会将 Observer 模式视为实现事件的复杂方式。
图 1 所示的解决方案显示从 Subject 类继承的 ConcreteSubject 类。在 Subject 类中,实现了添加或删除观察器以及遍历观察器列表的方法。ConcreteSubject 必须做的全部工作是继承 Subject,并在发生状态更改时调用 Notify()。在仅支持单一继承的语言(如 Java 或 C#)中,一个类如果继承了 Subject 就不能再继承任何其他类。这会是一个问题,因为在许多情况下 ConcreteSubject 是一个可能继承域对象基类的域对象。因此,将 Subject 类替换为 Subject 接口并为实现提供一个帮助器类是一个更好的主意(见图 3)。这样,您不担忧与 Subject 类的单一超类关系,而可以在另一个继承层次结构中使用 ConcreteSubject。一些语言(例如 Smalltalk)甚至将 Subject 接口实现为 Object 类的一部分,让每个类隐式继承 Subject 接口。
图 3:使用帮助器类避免继承 Subject 类
可惜的是,现在您必须将代码添加到从 Subject 接口继承的每个类中,才能实现在该接口中定义的方法。此任务可能是非常重复单调的。此外,因为域对象与 ConcreteSubject 重合,所以,它无法区分与不同主体关联的各种状态更改类型。这仅允许观察器订阅 ConcreteSubject 的所有状态更改,即使您可能希望选择某些更改(例如,如果源对象包含一个列表,那么,您可能希望得到更新通知,而不是插入通知)。您可能让观察器筛掉不相关的通知,但是,这样做会降低解决方案的效率,因为 ConcreteSubject 调用所有观察器只是为了确定它们是否真的不感兴趣。
通过将主体与源类完全分离,可以解决这些问题(见图 4)。这样做的结果是:ConcreteSubject 仅实现 Subject 接口;它没有任何其他职责。这样,DomainObject 就可以与多个 ConcreteSubject 关联,以便您可以区分单个域类的不同事件类型。
图 4:将 DomainObject 与 Subject 分离
.NET Framework 中的事件和委托功能以语言构造的形式实现了此方法,以便您甚至不必再实现自己的 ConcreteSubject 类。简单地说,事件替代了 ConcreteSubject 类,委托实现了 Observer 接口的角色。
传播状态信息
到此为止,此解决方案描述了客户端对象如何在发生状态更改时通知观察器,但还没有讨论观察器如何确定客户端对象所处的状态。有两种机制可以将此信息传递给观察器。
推模型。在 推模型中,由客户端发送有关主体状态更改的所有相关信息,再由主体将信息传递给每个观察器。如果信息是以中性格式(例如 XML)传递的,此模型就会使依赖性观察器不必直接访问客户端即可获取详细信息。另一方面,主体必须作出一些有关哪些信息与观察器相关的假定。如果添加了 新观察器,则主体可能必须发布该观察器所需的其他信息。这将使主体和客户端再次依赖于观察器,而这是您在前面试图解决的问题。因此,如果使用推模型,则在 确定要传递给观察器的信息量时,应该宁可错误地包括信息。在许多情况下,您将在对观察器的调用中包括对主体的引用。观察器可以使用该引用获得状态信息。
拉模型。在拉模型中,客户端将状态更改通知主体。观察器收到通知之后,它们使用 getState() 方法访问主体或客户端,以获取其他数据(见图 5)。此模型不要求主体将任何信息与 update() 方法一起进行传递,但是它可能要求观察器只是为了确定状态更改是否不相关而调用 getState()。因此,此模型的效率可能低一点。当观察器和主体在不同的线程中运行时(例如,如果使用 RMI 通知观察器),可能会出现另一个问题。在此情况下,在观察器通过回调获得状态信息之前,主体的内部状态可能已经再次更改。这可能导致观察器跳过操作。
图 5:使用接收模型的状态传播
何时触发更新
在实现 Observer 模式时,您可以选择两种方式之一来管理更新的触发。第一种方式是:在每次影响内部状态更改的 Subject 调用之后,在客户端中插入 Notify() 调用。这样,客户端就可以完全控制通知的频率;但同时也使客户端具有额外的职责,在开发人员忘记调用 Notify() 时这会引起错误。另一种方式是:在 Subject 的每个更改状态操作内封装 Notify() 调用。这样,状态更改始终会导致调用 Notify(),而客户端无需执行其他操作。不利方面是,几个嵌套的操作可能导致多个通知。图 6 显示这种情况的一个示例,其中操作 A 调用子操作 B,观察器可能收到两个对其 Update 方法的调用。
图 6:额外的通知
为单个但嵌套的操作而调用多个更新,会导致效率有一定程度的下降,而且产生更严重的副作用:如果在操作 B 结束时调用嵌套的 Notify 方法(见图 6),主体可能处于无效状态,因为仅仅处理了操作 A 的一部分。在这种情况下,应该避免嵌套的通知。例如,可以将操作 B 提取到一个没有通知逻辑的方法中,并且可以依赖操作 A 内对 Notify() 的调用。Template Method [Gamma95] 是一种确保仅通知一次观察器的有用构造。
影响状态更改的观察器
在一些情况下,观察器在处理 update() 调用的同时可能更改主体的状态。如果主体在每次状态更改之后都自动调用 Notify(),则可能引起问题。图 7 说明引起问题的原因。
图 7:从更新内修改对象状态导致无限循环
在此示例中,作为对状态更改通知的响应,观察器执行了操作 A。如果操作 A 更改 DomainObject 的状态,此后它将触发对 Notify() 的另一个调用,这又会再次调用观察器的 Update 方 法。这将导致无限循环。在此简单示例中无限循环是很容易识别的,但是如果关系是更复杂的,则可能很难确定依赖链。降低无限循环可能性的一种方法是使通知与特定兴趣有关。例如,在 C# 中,将下列接口用于主体,其中 Interest 可能是所有类型的兴趣的枚举:
interface Subject
{
public void addObserver(Observer o, Interest a);
public void notify(Interest a);
...
}
interface Observer
{
public void update(Subject s, Interest a);
}
仅当与观察器的特定兴趣有关的事件发生时才允许通知观察器,这样可以减小依赖链和帮助避免无限循环。这与在 .NET 中定义多个、所定义范围更狭窄的事件类型是等效的。避免循环的另一种方法是引入锁定机制,使主体在仍然处于原始 Notify() 循环中时无法发布新通知。
因为 Observer 支持松耦合和减少依赖性,是否应该让互相依赖的每对对象建立松耦合?当然不是。对于大多数模式,一种解决方案很少解决所有问题。在利用 Observer 模式时,您需要权衡下列因素。
优点
支持松耦合和减少依赖性。客户端不再依赖于观察器,因为通过使用主体和 Observer 接口对客户端进行了隔离。许多框架具有此优点,在这些框架中的应用程序组件可以注册为当(低级)框架事件发生时得到通知。结果,框架将调用应用程序组件,但不会依赖于它。
观察器数目可变。观察器可以在运行时附加和分离,因为主体对于观察器数目没有任何假定。此功能在这样的情况下是很有用的:观察器数在设计时是未知的。例如,如果用户在应用程序中打开的每个窗口都需要一个观察器。
缺点
性能降低。在许多实现中,观察器的 update() 方法可能与主体在同一线程中执行。如果观察器列表很长,则执行 Notify() 方法可能需要很长时间。抽取对象依赖性并不意味着添加观察器对应用程序没有任何影响。
内存泄漏。在 Observer 中 使用的回调机制(当对象注册为以后调用时)会产生一个常见的错误,从而导致内存泄漏,甚至是在托管的 C# 代码中。假定观察器超出作用范围,但忘记取消对主体的订阅,那么主体仍然保留对观察器的引用。此引用防止垃圾收集在主体对象也被破坏之前重新分配与观察器 关联的内存。如果观察器的生存期比主体的生存期短得多(通常是这种情况),则会导致严重的内存泄漏。
隐藏的依赖项。观察器的使用将显式依赖性(通过方法调用)转变为隐式依赖性(通过观察器)。如果在整个应用程序中广泛地使用观察器,则开发人员几乎不可能通过查看源代码来了解所发生的事情。这样,就使得了解代码更改的含意非常困难。此问题随传播级别急剧增大(例如,充当 Subject 的观察器)。因此,应该仅在少数定义良好的交互(如 Model-View-Controller 模式中模型和视图之间的交互)中使用观察器。最好不要在域对象之间使用观察器。
测试 / 调试困难。尽管松耦合是一项重大的体系结构功能,但是它可以使开发更困难。将两个对象去耦的情况越多,在查看源代码或类的关系图时了解它们之间的依赖性就越难因此,仅当可以安全地忽略两个对象之间的关联时才应该将它们松耦合。
在 .NET 中实现 Observer
在 Microsoft.NET 中构建一个应用程序,并且必须在不使源对象依赖于依赖性对象的情况下将状态更改通知该依赖性对象。
为了解释如何在 .NET 中实现 Observer (观察器),并说明限制对象之间的依赖性所获得的好处,下面的示例重构了一个具有双向依赖关系的解决方案。首先,将该解决方案重构为基于 Design Patterns中所定义的 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中所描述的解决方案来实现 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 类,这已经是很理想的了。
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 事件。这就使服务变得很复杂,因为服务会收到它们可能不感兴趣的事件。
降低了显式程度,提高了复杂性。现在取消了直接依赖性,但代码的显式程度降低了。在原来的实现中,Album 和 BillingService 有直接依赖性,因此,可以很容易地知道调用 GenerateCharge 方法的方式和时间。而在此示例中,Album 调用 Subject 中的 Notify 方法,该方法则通过一列以前注册的 Observer 对象进行迭代,然后再调用 Update 方法。在 BillingService 类中,Update 方法将调用 GenerateCharge。如果您想详细了解显式方式的优点,请参阅 Martin Fowler 在 IEEE Software [Fowler01] 中的文章“To Be Explicit”。
修改后的 Observer
Observer的主要缺点是,使用继承作为共享 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 解决方案解决了以前的解决方案存在的问题。实际上,对于只有单一实现继承的语言来说,这是首选的实现方法。不过,此解决方案仍然有以下缺点:
- 更加复杂。原来的解决方案由两个类组成,它们以显式方式直接相互引用,而现在的解决方案则有两个接口和三个类进行间接对话,并且还包括第一个示例中所没有的许多代码。毫无疑问,您开始考虑,原来的依赖性是否没有那么糟糕。不过,您应该记住,这两个接口和 SubjectHelper 类可以由任意多个观察器重用。因此,在整个应用程序中,它们可能只需要编写一次。
- 降低了显式性。此解决方案与 Observer [Gamma95] 一样,很难确定哪个观察器在观察 Subject 的更改。
因此,此解决方案是很好的面向对象设计,但需要创建许多类、接口、关联等等。
.NET 中的观察器
使用.NET 的内置功能,您只需少得多的代码就可以实现 Observer 模式。您不需要 Subject、SubjectHelper 和 Observer 类型,因为有了公共语言运行库,它们就已经过时了。在 .NET 中引入的委派和事件使您不必开发特定类型就能实现 Observer。
在基于 .NET 的实现中,事件代表了一种在“修改后的观察器”中所描述的 SubjectHelper 类的抽象(受公共语言运行库和各种编译器支持)。Album 类公开事件类型,而不是 SubjectHelper。观察器角色比以前要稍微复杂一些。观察器必须创建特定的委派实例,并向主体事件注册该委派,而不是实现 Observer 接口并向主体注册自身。观察器必须使用由事件声明所指定的类型的委派实例;否则,注册将失败。在此委派实例的创建期间,观察器提供将接受主体通知的方法名 (实例或静态)。当委派绑定到方法之后,它就可以向主体的事件进行注册。同样,也可以从事件注销此委派。主体通过调用事件向观察器提供通知。
下面的代码示例演示了为了使用委派和事件而必须对“修改后的观察器”中的示例所做的更改。
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 接口减少了观察器类和主体类之间的耦合,而委派的使用则完全取消了这种耦合。
进行综合衡量后,使用委派和事件模型在 .NET 中实现 Observer 所带来的优点显然超过了潜在的缺点。
优点
在 .NET 中实现 Observer 有以下优点:
取消了依赖性。上述示例清楚地显示出 Album 和 BillingService 类之间的依赖性已经取消。
提高了可扩展性。“.NET 中的观察器”示例说明了添加新类型的观察器是多么简单。Album 类是“开-闭”原则的一个例子,最初是 Bertrand Meyer 在 Object-Oriented Software Construction 第二版 [Bertrand00] 中编写的,它描述了一个易于扩充但不必修改的类。Album 类体现了这种原则,因为您可以添加 PlayEvent 的观察器,而不必修改 Album 类。
提高了可测试性。“测试考虑事项”说明了您为什么可以不必对 BillingService 进行实例化就能测试 Album 类。测试验证了 Album 类能正确运行。测试还提供了如何编写 BillingService 的出色示例。
缺点
Observer 的实现简单而直接。不过,随着委派和事件的数目不断增加,我们很难跟踪当事件触发时发生了什么情况。因此,代码变得很难调试,因为您必须在代码中搜索观察器。
模型-视图-控制器
许多计算机系统的用途都是从数据存储检索数据并将其显示给用户。在用户更改数据之后,系统再将更新内容存储到数据存储中。因为关键的信息流发生在数据存储和用户界面之间,所以您可能倾向于将这两部分绑在一起,以减少编码量并提高应用程序性能。但是,这种看起来自然而然的方法有一些大问题。一个问题是,用户界 面的更改往往比数据存储系统的更改频繁得多。
问题
如何让 Web 应用程序的用户界面功能实现模块化,以便您可以轻松地单独修改各个部分?
影响因素
下列影响因素作用于此上下文内的系统,在考虑问题的解决方案时必须协调这些因素
1、用户界面逻辑的更改往往比业务逻辑频繁,尤其是在基于 Web 的应用程序中。
2、在某些情况下,应用程序以不同的方式显示同一数据。
3、设计令人赏心悦目而有效的 HTML 页通常要求采用一套与开发复杂业务逻辑不同的技能。很少有人同时具有这两种技能。因此,将这两部分的开发工作分隔开来是最理想的。
4、用户界面活动通常由以下两部分组成:显示和更新。显示部分负责从数据源检索数据,并格式化数据以便进行显示。当用户基于该数据执行操作时,更新部分将控制权返回给业务逻辑,以便更新数据。
5、在 Web 应用程序中,单个页面请求将这两方面的工作组合在一起:与用户所选链接相关联的操作进行的处理,以及目标页面的显示。在许多情况下,目标页可能不与操作直 接相关。
6、与业务逻辑相比,用户界面代码对设备的依赖性往往更大。如果要将应用程序从基于浏览器的应用程序迁移到个人数字助理 (PDA) 或支持 Web 的手机上,则必须替换很多用户界面代码,而业务逻辑可能不受影响。这两部分的完全分离可以使迁移更快完成,并最大限度地降低将错误引入业务逻辑的风险。
7、通常,为用户界面创建自动测试比为业务逻辑更难、更耗时。因此,减少直接绑到用户界面中的代码量可提高应用程序的可测试性。
解决方案
Model-View-Controller (MVC) 模式基于用户输入将域的建模、显示和操作分为三个独立的类:
1、模型。模型用于管理应用程序域的行为和数据,并响应为获取其状态信息(通常来自视图)而发出的请求,还会响应更改状态的指令(通常来自控制器)。
2、视图。视图用于管理信息的显示。
3、控制器。控制器用于解释用户的鼠标和键盘输入,以通知模型和/或视图进行相应的更改。
图 1 描述了这三个对象之间的结构关系。
图 1:MVC 类结构
请务必注意,视图和控制器都依赖于模型。但是,模型既不依赖于视图,也不依赖于控制器。这是分离的主要优点之一。这样的分离允许模型在独立于可视表示功能的情况下建立和测试。在许多胖客户端应用程序中,视图与控制器的分离是次要的,实际上,许多用户界面框架将角色实现为一个对象。另一方面,在 Web 应用程序中,视图(浏览器)与控制器(处理 HTTP 请求的服务器端组件)的分离是很好定义的。
Model-View-Controller 是一个用于将用户界面逻辑与业务逻辑分离开来的基础设计模式。遗憾的是,此模式的普及导致了许多错误的描述。特别是在不同的上下文中,术语"控制器"已经用于意指不同的事物。幸运的是,Web 应用程序的出现已经帮助消除了一些不明确性,因为视图与控制器的分离是如此明显。
变型
当一个控制器以独占方式操作模型时,将使用被动模型。控制器将修改模型,然后通知视图:模型已经更改,应该进行刷新(见图 2)。此情况下的模型完全独立于视图和控制器,这意味着模型无法报告其状态更改。HTTP 协议是此方案的示例。浏览器没有从服务器获取异步更新的简单方法。浏览器显示视图并对用户输入作出响应,但是它不会检测服务器上的数据更改。仅当用户显式请求刷新时,才会询问服务器是否发生了更改。
图 2:被动模型的行为
当模型更改状态而不涉及控制器时,将使用主动模型。当其他资源正在更改数据并且更改必须反映在视图中时,可能会发生这种情况。以股票报价机的显示为例。您从 外部源接收股票数据,并希望当股票数据更改时更新视图(例如,报价机数据区和警告窗口)。因为只有模型检测对其内部状态的更改(在这些更改发生时),所以模型必须通知视图刷新显示。
但是,使用 MVC 模式的一个目的是使模型独立于视图。如果模型必须将更改通知视图,则会重新带来您希望避免的依赖性。幸运的是,Observer 模式提供了这样的机制:提醒其他对象注意状态的更改,而不会导致对这些对象的依赖性。各个视图实现 Observer 接口,并向模型注册。模型将跟踪由订阅更改的所有观察器组成的列表。当模型发生改变时,模型将会遍历所有已注册的观察器,并将更改通知它们。此方法通常称 为"发布-订阅"。模型从不需要有关任何视图的特定信息。实际上,在需要将模型更改通知控制器的情况下(例如,启用或禁用菜单选项),控制器必须做的全部工作是实现 Observer 接口并订阅模型更改。对于存在许多视图的情况,定义多个主体是有意义的,其中每个主体都描述了特定类型的模型更改。然后,每个视图都只能订阅与视图有关的 更改类型。
图 3 显示了使用 Observer 的主动 MVC 的结构,以及观察器如何将模型与直接引用视图隔离开来。
图 3:在主动模型中使用观察器将模型与视图分离
图 4 说明当模型发生改变时 Observer 如何通知视图。可惜的是,在统一建模语言 (UML) 序列图中,没有好的方法来演示模型与视图的分离,因为该图表示的是对象的实例而不是类和接口。
图 4:主动模型的行为
测试考虑事项
在使用 Model-View-Controller 时,可测试性有了极大提高。当组件(尤其是用户界面组件)相互高度依赖时,测试组件变得很困难。这些类型的组件通常需要复杂的设置,只是为了测试简单的功 能。更糟的是,在出现错误时,很难确定该问题发生在哪个特定组件上。这就是为什么将任务分离开来是重要的体系结构级别动机。MVC 将存储、显示和更新数据的任务分隔到三个可以分别进行测试的组件中。
除因互相依赖而产生的问题之外,用户界面框架本身是很难测试的。测试用户界面需要冗长乏味的(且易于出错)手动测试,或需要模拟用户操作的测试脚本。这些脚本的开发往往很耗时,而且容易被破坏。MVC 没有消除对用户界面测试的需要,但是,通过将模型与显示逻辑分离,可以允许模型在独立于显示的情况下进行测试,并减少了用户界面的测试步骤数。
围绕 MVC 模式构建显示层具有下列优缺点:
优点
1、支持多个视图。因为视图与模型分离,而且模型与视图之间没有直接依赖性,所以用户界面可以同时显示同一数据的多个视图。例如,Web 应用程序中的多个页面可以使用同一模型对象。另一个示例是允许用户对页面外观进行更改的 Web 应用程序。这些页面显示来自共享模型的同一数据,但以不同的方式进行显示。
2、适应更改。用户界面要求的更改往往比业务规则快。用户可能更喜欢新设备(如手机或 PDA)采用另一颜色、字体、屏幕布局和支持级别。因为模型不依赖于视图,所以将新类型的视图添加到系统中通常不会影响模型。因此,更改的作用范围仅限于视图。此模式为其进一步的专门化模式(如 Page Controller 和Front Controller)奠定了基础。
缺点
1、复杂性。MVC 模式引入了新的间接级别,因此稍微增加了解决方案的复杂性。它还增加了用户界面代码的事件驱动特性,调试用户界面代码会变得更加困难。
2、频繁更新的成本。将模型与视图分离并不意味着模型的开发人员可以忽略视图的特性。