微软顶级技术大师Jeffrey Richter的作品,一向是不容错过的。为了帮助开发者这篇专论Observer模式的文章也不例外。Observer模式是经典设计模式中应用最为广泛也最为灵活多变的模式之一。本文在.NET技术框架下深入发掘了Observer模式的内涵,值得细细品味。
虽然设计模式并不是万能丹,但确实是一个非常强大的工具,开发人员或架构师可使用它积极地参与任何项目。设计模式可确保通过熟知和公认的解决方案解决常见问题。模式存在的事实基础在于:大多数问题,可能已经有其他个人或开发小组解决过了。因此,模式提供了一种在开发人员和组织之间共享可使用解决方案的形式。无论这些模式的出处是什么,这些模式都利用了大家所积累的知识和经验。这可确保更快地开发正确的代码,并降低在设计或实现中出现错误的可能性。此外,设计模式在工程小组成员之间提供了通用的术语。参加过大型开发项目的人员都知道,使用一组共同的设计术语和准则对成功完成项目来说是至关重要的。最重要的是,如果能正确地使用,设计模式可以节省您大量的时间。
.NET框架模式
虽然GoF的示例仅限于C 和Smalltalk,但设计模式并不与特定语言或开发平台捆绑在一起;Microsoft .NET框架的出现为分析设计模式提供了新的机会和环境。在框架类库(FCL)的开发过程中,Microsoft应用了很多GoF模式。由于.NET框架中提供的功能范围非常广泛,因此,还开发和提出了一些全新的模式。
我们对设计模式的研究从Observer模式入手。
Observer模式
面向对象的开发的一个主导原则是,在给定的应用程序中正确地划分任务。系统中的每个对象应该将重点放在问题域中的离散抽象上。简而言之,一个对象只应做一件事,而且要将它做好。这种方法可确保在对象之间划定清晰的界限,因而可提供更高的重用性和系统可维护性。
一个特别重要的领域是用户界面和基础业务逻辑之间的交互。在应用程序的开发过程中,需要快速更改用户界面,并且不能对应用程序的其他部分产生连带影响,这是司空见惯的事。此外,业务要求也可能会发生变化,而这一切与用户界面无关。具有丰富开发经验的人都知道,在很多情况下,这两组要求都会发生变化。如果没有划分UI和应用程序其他部分,修改任一部分都会对整体造成不利的影响。
很多应用程序都需要在用户界面和业务逻辑之间划分清晰的界限。因此,自GUI出现以后,很多面向对象的框架均支持将用户界面从应用程序的其他部分中划分出来。其中的大部分应用程序采用的设计模式几乎相同。这种模式通常称为观察者,它非常有助于在系统中各种对象之间划分清晰的界限。此外,还会经常看到在框架或应用程序中与UI无关的部分中使用这种解决方案。Observer模式的作用远远超过了其最初的想法。
逻辑模型
虽然Observer模式有很多变体,但该模式的基本前提包含两个角色:观察者(observer)和主体(subject)(熟悉Smalltalk MVC的人将这些术语分别称为View和Model)。在用户界面的环境中,观察者是负责向用户显示数据的对象。另一方面,主体表示从问题域中模拟的业务抽象。正如图1中所描述的一样,在观察者和主体之间存在逻辑关联。当主体对象中发生更改时,(例如,修改实例变量),观察者就会观察这种更改,并相应地更新其显示。
例如,假定我们要开发一种简单的应用程序,来跟踪全天的股票价格。在此应用程序中,我们指定一个Stock类来模拟在NASDAQ交易的各种股票。该类包含一个实例变量,它表示在全天不同时段经常波动的股价。为了向用户显示此信息,应用程序使用一个StockDisplay类向stdout(标准输出)写信息。在此应用程序中,一个Stock类实例作为主体,一个StockDisplay类实例作为观察者。随着股价在交易日中随时间发生变化,Stock实例的当前股价也会发生变化(它怎样变化并不重要)。因为StockDisplay实例正在观察Stock实例,所以在这些状态发生变化(修改股价)时,就会向用户显示这些变化。
通过使用这种观察过程,可以在Stock和StockDisplay类之间划分界限。假定应用程序的要求第二天发生变化,要使用基于窗体的用户界面。要启用此新功能,只需要构造一个新类StockForm作为观察者。无论发生什么情况,Stock类都不需要进行任何修改。事实上,它甚至不知道发生此类更改。类似地,如果需求变化要求Stock类从另一个来源检索股价信息(可能是从Web服务,而不是从数据库中检索),则StockDisplay类不需要进行修改。它只是继续观察Stock就够了。
物理模型
正如大多数解决方案一样,问题在于细节。Observer模式也不例外。虽然逻辑模型规定观察者观察主体;但在实现这种模式时,这实际上是一个名称误用。更准确地说,观察者向主体注册,表明它观察主体的意愿。在某种状态发生变化时,主体向观察者通知这种变化情况。当观察者不再希望观察主体时,观察者向主体撤消注册。这些步骤分别称为观察者注册、通知和撤消注册。
大多数框架通过回调来实现注册和通知。图2、3和4中所示的UML序列图模拟这种方法通常使用的对象和方法调用。对于不熟悉序列图的人来说,最上面的矩形框表示对象,而箭头表示方法调用。
图2描述了注册序列。观察者对主体调用Register方法,以将其自身作为参数传递。在主体收到此引用后,它必须将其存储起来,以便在将来某个时间状态发生变化时通知观察者。大多数观察者实现并非将观察者引用直接存储在实例变量中,而是将此任务委托给一个单独的对象(通常为一个容器)。使用容器来存储观察者实例可提供非常大的好处,我们将对它进行简要介绍。
图3突出显示了通知序列。当状态发生变化时(AskPrice
Changed),主体通过调用Get观察者s方法来检索容器中的所有观察者。主体然后枚举检索的观察者,并调用Notify方法以通知观察者所发生的状态变化。
图4显示撤消注册序列。此序列是在观察者不再需要观察主体时执行的。观察者调用UnRegister方法,并将其自身作为参数进行传递。然后,主体对容器调用Remove方法以结束观察过程。
回到我们的股票应用程序,让我们分析一下注册和通知过程所产生的影响。在应用程序启动过程中,一个StockDisplay类实例注册到Stock实例中,并将其自身作为参数传递到Register方法。Stock实例(在容器中)保存对StockDisplay实例的引用。当股价属性发生变化时,Stock实例通过调用Notify方法向StockDisplay通知所发生的变化。在应用程序关闭时,StockDisplay实例使用以下方法撤消注册Stock实例:调用UnRegister方法,终止两个实例之间的关系。
请注意利用容器(而不是使用实例变量)来存储观察者引用有什么优点。假定除当前用户接口StockDisplay外,我们还需要绘制股价在交易日内变化的实时图形。为此,我们创建了一个名为StockGraph的新类,它绘制股价(y轴)和当天时间(x轴)的图形。在应用程序启动时,它同时在Stock实例中注册StockDisplay和StockGraph类的实例。因为主体在容器(与实例变量相对)中存储观察者,所以这不会出现问题。当股价发生变化时,Stock实例向其容器中的两个观察者实例通知所发生的状态变化。正如我们所看到的一样,使用容器可提供更大的灵活性,即每个主体可支持多个观察者。这使主体有可能向无数多个观察者通知所发生的状态变化,而不是只通知一个观察者。
虽然不是强制要求,但很多框架为观察者和主体提供了一组要实现的接口。正如下面的C#代码示例所示,IObserver接口公开一种公共方法Notify。此接口是由所有要用作观察者的类实现的。IObservable接口(是由所有要用作主体的类实现的)公开两种方法Register和UnRegister。这些接口通常采用抽象虚拟类或真实接口的形式(如果实现语言支持此类构造的话)。利用这些接口有助于减少观察者和主体之间的耦合关系。与观察者和主体类之间的紧密耦合关系不同,IObserver和IObservable接口允许执行独立于实现的操作。通过对接口的分析,您将注意到键入的所有方法针对的是接口类型(与具体类相对)。这种方法将接口编程模型的优点扩展到Observer模式。
IObserver和IObservable接口(C#)
//interface the all observer classes should implement
public interface IObserver {
void Notify(object anObject);
}//IObserver
//interface that all observable classes should implement
public interface IObservable {
void Register(IObserver anObserver);
void UnRegister(IObserver anObserver);
}//IObservable
再回到我们的示例应用程序,我们知道Stock类用作主体。因此,它将实现IObservable接口。类似地,StockDisplay类实现IObserver接口。因为所有操作都是由该接口定义的(而不是由具体类定义的),所以Stock类并未与StockDisplay类绑定在一起,反之亦然。这使我们能够快速地更改特定的观察者或主体实现,而不会影响应用程序的其他部分(使用不同的观察者替换StockDisplay或添加额外的观察者实例)。
除了这些接口外,框架还经常为主体提供一个通用基类,减少了支持Observer模式所需的工作。基类实现IObservable接口,以提供支持观察者实例存储和通知所需的基础结构。下面的C#代码示例简要介绍一个名为ObservableImpl的基类。尽管可能任何容器都可以完成这一任务,但该类在Register和UnRegister方法中将观察者存储委托给哈希表实例(为了方便起见,我们在示例中使用哈希表作为容器,它只使用一个方法调用来撤消注册特定的观察者实例)。还要注意添加了NotifyObservers方法。此方法用于通知哈希表中存储的观察者。在调用此方法时,将枚举该容器,并对观察者实例调用Notify方法。
ObservableImpl类(C#)
//helper class that implements observable interface
public class ObservableImpl:IObservable {
//container to store the observer instance (is not synchronized for
this example)
protected Hashtable _ObserverContainer=new Hashtable();
//add the observer
public void Register(IObserver anObserver){
_ObserverContainer.Add(anObserver,anObserver);
}//Register
//remove the observer
public void UnRegister(IObserver anObserver){
_ObserverContainer.Remove(anObserver);
}//UnRegister
//common method to notify all the observers
public void NotifyObservers(object anObject) {
//enumeration the observers and invoke their notify method
foreach(IObserver anObserver in _ObserverContainer.Keys) {
anObserver.Notify(anObject);
}//foreach
}//NotifyObservers
}//ObservableImpl
我们的示例应用程序使用以下方法来利用此基类基础结构:修改Stock类以扩展ObservableImpl类,而不是提供其自己的特定IObservable接口实现。因为ObservableImpl类实现了IObservable接口,所以不需要对StockDisplay类进行任何更改。实际上,这种方法简化了Observer模式的实现,在保持类之间松散耦合关系的同时,使多个主体重复使用相同的功能。
下面的.NET观察者示例重点说明了IObservable和IObserver接口以及ObservableBase类在我们的股票应用程序中的使用情况。除了Stock和StockDisplay类外,此示例使用MainClass将观察者和主体实例关联起来,并修改Stock实例的AskPrice属性。此属性负责调用基类的NotifyObservers方法,而该方法又向该实例通知相关的状态变化。
观察者示例(C#)
//represents a stock in an application
public class Stock:ObservableImpl {
//instance variable for ask price
object _askPrice;
//property for ask price
public object AskPrice {
set { _askPrice=value;
base.NotifyObservers(_askPrice);
}//set
}//AskPrice property
}//Stock
//represents
the user interface in the application
public class StockDisplay:IObserver {
public void Notify(object anObject){
Console.WriteLine("The new ask price is:" anObject);
}//Notify
}//StockDisplay
public class MainClass{
public static void Main() {
//create new display and stock instances
StockDisplay stockDisplay=new StockDisplay();
Stock stock=new Stock();
//register the grid
stock.Register(stockDisplay);
//loop 100 times and modify the ask price
for(int looper=0;looper < 100;looper ) {
stock.AskPrice=looper;
}
//unregister the display
stock.UnRegister(stockDisplay);
}//Main
}//MainClass
.NET框架中的Observer模式
基于我们对Observer模式的了解,让我们将注意力转向此模式在.NET框架中的使用情况。您们当中非常熟悉FCL中所公开类型的人将会注意到,框架中没有IObserver、IObservable或ObservableImpl类型。虽然您的确可以在.NET应用程序中使用这些构造,但引入委托和事件可提供新的、功能强大的方法来实现Observer模式,而不必开发专用于支持该模式的特定类型。事实上,因为委托和事件是CLR的一级成员,所以将此模式的基本构造添加到.NET框架的核心中。因此,FCL在其结构中广泛使用Observer模式。
介绍委托和事件内部工作方式的文章非常多,我们在此不再赘述。我们只需说明委托是面向对象(和类型安全)版的函数指针就够了。委托实例保存对实例或类方法的引用,允许匿名调用绑定方法。事件是在类上声明的特殊构造,可在运行时发布被关注对象的状态变化。事件表示我们前面用于实现Observer模式的注册、撤消注册和通知方法的形式抽象(CLR和多种不同的编译器对它提供支持)。委托是在运行时注册到特定事件中的。在引发事件时,将调用所有注册的委托,以使它们能够收到事件的通知。
按照Observer模式定义的术语,声明事件的类就是主体。与我们以前使用的IObservable接口和ObservableImpl类不同,主体类不需要实现给定接口或扩展基类。主体只需要公开一个事件,而不需要执行任何其他操作。观察者创建的工作略多一些,但灵活性却提高得非常多(我们将在后面讨论)。观察者并不实现IObserver接口和将其自身注册到主体中,而是必须创建特定的委托实例,并将此委托注册到主体事件中。观察者必须使用具有事件声明所指定类型的委托实例,否则,注册就会失败。在创建此委托实例的过程中,观察者将传递该主体向委托通知的方法(实例或静态)名称。在将委托绑定到方法后,可以将其注册到主体的事件中。类似地,也可以从事件中撤消注册此委托。主体通过调用事件向观察者提供通知。
如果您不熟悉委托和事件,则实现Observer模式似乎需要做很多工作,尤其是与我们以前使用的IObserver和IObservable接口相比。但是,它比听起来要简单一些,并且实现起来要容易得多。下面的C#和Visual Basic .NET代码示例重点说明了在我们的示例应用程序中支持委托和事件所需的类修改。注意,没有Stock或StockDisplay类用于支持该模式的任何基类或接口。
使用委托和事件的观察者(C#)
public class Stock {
//declare a delegate for the event
public delegate void AskPriceDelegate(object aPrice);
//declare the event using the delegate
public event AskPriceDelegate AskPriceChanged;
//instance variable for ask price
object _askPrice;
//property for ask price
public object AskPrice {
set {
//set the instance variable
_askPrice=value;
//fire the event
AskPriceChanged(_askPrice);
}
}//AskPrice property
}//Stock class
//represents the user interface in the application
public class StockDisplay {
public void AskPriceChanged(object aPrice) {
Console.Write("The new ask price is:" aPrice "/r/n"); }
}//StockDispslay class
public class MainClass {
public static void Main(){
//create new display and stock instances
StockDisplay stockDisplay=new StockDisplay();
Stock stock=new Stock();
//create a new delegate instance and bind it
//to the observer's askpricechanged method
Stock.AskPriceDelegate aDelegate=new
Stock.AskPriceDelegate(stockDisplay.AskPriceChanged);
//add the delegate to the event
stock.AskPriceChanged =aDelegate;
//loop 100 times and modify the ask price
for(int looper=0;looper < 100;looper ) {
stock.AskPrice=looper;
}
//remove the delegate from the event
stock.AskPriceChanged-=aDelegate;
}//Main
}//MainClass
在熟悉了委托和事件后,您就会清楚地看到它们的巨大潜力。与IObserver和IObservable接口以及ObservableImpl类不同,使用委托和事件可大大减少实现此模式所需的工作量。CLR和编译器为观察者容器管理提供了基础,并且为注册、撤消注册和通知观察者提供了一个通用调用约定。也许,委托的最大优点是其能够引用任何方法的固有特性(条件是它符合相同的签名)。这允许任何类用作观察者,而与它所实现的接口或它专用的类无关。虽然使用IObserver和IObservable接口可减少观察者和主体类之间的耦合关系,但使用委托可完全消除这些耦合关系。
事件模式
基于事件和委托,FCL可以非常广泛地使用Observer模式。FCL的设计者充分认识到此模式的巨大潜力,并在整个框架中将其应用于用户界面和非UI特定的功能。但是,用法与基本Observer模式稍有不同,框架小组将其称为事件模式。
通常,将此模式表示为事件通知进程中所涉及的委托、事件和相关方法的正式命名约定。虽然CLR或标准编译器并没有强制要求利用事件和委托的所有应用程序和框架都采用这种模式,但Microsoft建议这样做。
其中的第一条约定也可能是最重要的约定是主体公开的事件的名称。对于它所表示的状态变化而言,此名称应该是不言自明的。切记,此约定以及所有其他此类约定本身就是主观性的。目的是为那些利用您的事件的人员提供清晰的说明。事件模式的其他部分利用正确的事件命名,因而此步骤对模式来说至关重要。
回到我们的示例,让我们分析一下这种约定对Stock类产生的影响。派生事件名称的适当方法是,利用在主体类中修改的字段的名称作为根。因为在Stock类中修改的字段名称是_askPrice,所以合理的事件名称应该是AskPriceChanged。很明显,此事件的名称比StateChangedInStockClass等具有更强的描述性。因此,AskPriceChanged事件名称符合第一条约定。
事件模式中的第二条约定是正确命名委托及其签名。委托名称应该包含事件名称(通过第一个约定选择的)及附加词Handler。此模式要求委托指定两个参数,第一个参数提供对事件发送方的引用,第二个参数向观察者提供环境信息。第一个参数的名称就是sender。必须将此参数键入为System.Object。这是由于以下事实:可能将委托绑定到系统中任何类上的任何潜在方法。第二个参数的名称(甚至比第一个参数更简单)为e。必须将此参数键入为System.EventArgs或某种派生类(有时比此内容还多)。虽然委托的返回类型取决于您的实现需要,但大多数实现此模式的委托根本不返回任何值。
需要稍加注意委托的第二个参数e。此参数允许主体对象将任意环境信息传递给观察者。如果不需要此类信息,则使用System.EventArgs实例就足够了,因为此类的实例表示没有环境数据。否则,应该使用相应的实现构造从System.EventArgs派生的类以提供此数据。必须按照具有附加词EventArgs的事件名称来命名该类。
请参考我们的Stock类,此约定要求将处理AskPriceChanged事件的委托命名为AskPriceChangedHandler。此外,应该将此委托的第二个参数命名为AskPriceChangedEventArgs。因为我们需要将新的股价传递给观察者,所以我们需要扩展System.EventArgs类,以将该类命名为AskPriceChangedEventArgs并提供实现来支持传递此数据。
事件模式中的最后一个约定是负责引发事件的主体类上方法的名称和可访问性。此方法的名称应该包含事件名称以及添加的On前缀。应该将此方法的可访问性设置为保护。此约定仅适用于非密封(在VB中不可继承)类,因为它作为派生类调用在基类中注册的观察者的已知的调用点。
将此最后一条约定应用于Stock类,即可完成事件模式。因为Stock类不是密封的,所以我们必须添加一种方法来引发事件。按照该模式,此方法的名称为OnAskPriceChanged。下面的C#代码示例显示应用于Stock类的事件模式的完整视图。请注意我们的System.EventArgs类的专门用法。
事件模式示例(C#)
public class Stock {
//declare a delegate for the event
public delegate void AskPriceChangedHandler(object sender,
AskPriceChangedEventArgs e);
//declare the event using the delegate
public event AskPriceChangedHandler AskPriceChanged;
//instance variable for ask price
object _askPrice;
//property for ask price
public object AskPrice {
set {
//set the instance variable
_askPrice=value;
//fire the event
OnAskPriceChanged();
}
}//AskPrice property
//method to fire event delegate with proper name
protected void OnAskPriceChanged() {
AskPriceChanged(this,new AskPriceChangedEventArgs(_askPrice));
}//AskPriceChanged
}//Stock class
//specialized event class for the askpricechanged event
public class AskPriceChangedEventArgs:EventArgs {
//instance variable to store the ask price
private object _askPrice;
//constructor that sets askprice
public AskPriceChangedEventArgs(object askPrice) { _askPrice=askPrice; }
//public property for the ask price
public object AskPrice { get { return _askPrice; } }
}//AskPriceChangedEventArgs
结论
基于这里对Observer模式的分析,我们可以清楚地看到此模式提供了一个完美的机制,能够在应用程序中的对象之间划定清晰的界限。虽然通过回调进行实现(使用IObserver和IObservable接口)相当简单,但CLR的委托和事件概念可处理大多数“繁重的工作”,并降低主体和观察者之间的耦合级别。实际上,通过正确地使用此模式,在确保应用程序可演变性方面就会向前迈出一大步。当您的UI和业务要求随时间发生变化时,Observer模式可确保能够简化您的工作。
在开发灵活的应用程序方面,设计模式是一个非常强大的工具(如果有效地加以运用)。撰写本文是为了说明模式方法的有效性,并重点说明.NET框架中使用的一种模式。将来的文章将继续探究FCL中的模式,并简要介绍一些用于生成有效Web服务的模式。