观察者模式--不少理论阐述--摘抄他人的作品
软件设计模式的起源归因于 Christopher Alexander 所做的工作。 作为架构师,
Alexander 注意到在给定的环境中存在常见问题及其相关的解决方案。 Alexander 将此
问题/解决方案/环境三元组称为“设计模式”,架构师在构建设计过程中可通过它以统一
的方式快速解决问题。 二十五年前第一次出版的 A Pattern Language: Towns,
Buildings, Construction (Alexander 等人编著,牛津大学出版社于 1977 年出版)介
绍了 250 多种建筑设计模式,并提供了将此概念融入软件开发领域的基本原则。
在 1995 年,软件业首次广泛采用了设计模式,因为它们与构建应用程序直接相关。 四
位作者 Gamma、Helm、Johnson 和 Vlissides(统称为四人组或 GoF)将 Alexander 的
设计模式与他们的作品 Design Patterns: Elements of Reusable Object-Oriented
Software (Addison-Wesley 出版公司于 1995 年出版)中的刚刚兴起的面向对象的软件
开发动向结合起来。 凭着他们丰富的经验和对现有对象框架的分析,GoF 提供了 23 种
设计模式,这些模式分析了在设计和构造应用程序时遇到的常见问题和解决方案。 在这
个出版物之后,设计模式的概念已发展为包含在软件领域中遇到的很多 问题和解决方案
。 事实上,设计模式的广泛采用导致了反模式概念的出现,这些模式是指通常会使手头
问题更加严重而不加解决问题的解决方案。
为什么要使用设计模式?
虽然设计模式并不是万能钥匙(如果世上真有此物的话),但它是一个非常强大的工具,
开发人员或架构师可使用它积极地参与任何开发项目。 设计模式可确保通过熟知和公认
的解决方案解决常见问题。 模式存在的事实基础在于:大多数问题其他个人或开发小组
可能已经遇到并解决了。 因此,模式提供了一种在开发人员和组织之间共享可使用的解
决方案的机制。 无论这些模式的出处是什么,这些模式都利用了大家所积累的知识和经
验。 这可确保更快地开发正确的代码,并降低在设计或实现中出现错误的可能性。 此外
,设计模式在工程小组成员之间提供了通用的语义。 参加过大型开发项目的人员都知道
,使用一组共同的设计术语和准则对成功完成项目来说是至关重要的。 最重要的是,设
计模式可以节省您大量的时间 (如果正确使用的话)。
观察者模式
面向对象的开发的一个主导原则是,在给定的应用程序中正确地分配任务。 系统中的每
个对象应该将重点放在问题域中的离散抽象上,而不是放在任何其它方面。 简而言之,
一个对象只应做一件事,而且要将它做好。 这种方法可确保在对象之间划定清晰的界限
,因而可提供更高的重用性和系统可维护性。
一个正确划分任务特别重要的领域就是,用户界面和基础业务逻辑之间的交互。 在应用
程序的开发过程中,需要快速更改用户界面要求,并且不会对应用程序的其他部分产生连
带影响,这是司空见惯的事。 此外,业务要求也可能会发生变化,而这一切与用户界面
无关。 具有丰富开发经验的人都知道,在很多情况下,这两组要求都会发生变化。 如果
没有划分 UI 和应用程序其他部分,修改任一部分都会对整体造成不利的影响。
很多应用程序都会遇到以下常见问题:需要在用户界面和业务逻辑之间划分清晰的界限。
因此,自 GUI 出现以后开发的很多面向对象的框架均支持将用户界面从应用程序的其他
部分中划分出来。 不要惊讶(可能有一点),其中的大部分应用程序采用类似的设计模
式来提供此功能。 这种模式通常称为观察者,它在系统中的各种对象之间划分清晰的界
限方面非常有利。 此外,还会经常看到在框架或应用程序中与 UI 无关的部分中使用这
种解决方案。 正如大多数其他模式一样,观察者模式的作用远远超过了其最初的想法。
虽然观察者模式有很多变体,但该模式的基本前提包含两个角色:
观察者和主体
(熟悉 Smalltalk MVC 的人将这些术语分别称为视图和模型)。 在用户界面的环境中,
观察者是负责向用户显示数据的对象。 另一方面,主体表示从问题域中模拟的业务抽象
。 在观察者和主体之间存在逻辑关联。 当主体对象中发生更改时,(例如,修改实例变
量),观察者就会观察 这种更改,并相应地更新其显示。
//。。。。。。。。。。。。。。。。。。。。。。。。。。。。
//观察者如何观察到这种变话呢?主体在放声改变后调用观察者的一个函数来告诉观察者
它变化了。或者给 中介发一个他发生改变的消息然后中介再告诉观察者这个事情。
在这个过程中,观察者可能需要采用类族的方式,也就是所有观察者继承同一个接口
例如,假定我们要开发一种简单的应用程序,来跟踪全天的股票价格。 在此应用程序中
,我们指定一个 “常用” 类来模拟在 NASDAQ 交易的各种股票。 该类包含一个实例变
量,它表示在全天不同时段经常波动的当前询价。 为了向用户显示此信息,应用程序使
用一个 StockDisplay 类向 stdout(标准输出)写入信息。 在应用程序中,一个 “常
用” 类实例作为主体,一个 StockDisplay 类实例作为观察者。 随着询价在交易日中随
时间发生变化,“常用” 实例的当前询价也会发生变化(它怎样变化并不重要)。 因为
StockDisplay 实例正在观察“常用” 实例,所以在这些状态发生变化(修改询价)时,
就会向用户显示这些变化。
通过使用这种观察过程,可确保在 “常用” 和 StockDisplay 类之间划分界限。 假定
应用程序的要求第二天发生变化,因而要求使用基于窗体的用户界面。 要启用此新功能
,只需要构造一个新类 StockForm 作为观察者。 无论发生什么情况,“常用” 类都不
需要进行任何修改。 事实上,它甚至不知道发生此类更改。 类似地,如果需求变化要求
“常用” 类从另一个来源检索询价信息(可能是从 Web 服务,而不是从数据库中检索)
,则 StockDisplay 类不需要进行修改。 它只是继续观察 “常用”,而并不注意发生的
任何变化。
物理模型
正如大多数解决方案一样,难题出在细节上。 观察者模式也不例外。 虽然逻辑模型规定
观察者观察主体;但实现这种模式时,这实际上是一个名称误用。 更准确地说,观察者
注册主体,表明它对观察的意向。 在某种状态发生变化时,主体向观察者通知这种变化
情况。 当观察者不再希望观察主体时,观察者从主体中撤消注册。 这些步骤分别称为观
察者注册、通知和撤消注册。
大多数框架通过回调来实现注册和通知。
观察者调用主体的Register 方法,以将其自身(观察者)作为参数传递。 在主体收到此
引用后,必须将其存储起来,以便在将来某个时间状态发生变化时通知观察者。
大多数主体实现并非将观察者引用直接存储在实例变量中,而是将此任务委派给一个单独
的对象(通常为一个容器)。 也就是主体把观察者保存在容器中。可以让所有的主体共
享一个容器。 使用容器来存储观察者实例可提供非常大的好处。
当然存在一种情况 那就是很多个观察者共同观察一个主体。所以可以让一个主体独占一
个容器(按值聚合)。
当主体状态发生变化时 (AskPriceChanged),主体通过调用 GetObservers 方法来检索容
器中的所有观察者。 主体然后枚举检索的观察者,并调用 “通知” 方法以通知观察者
所发生状态变化。
在观察者不再需要观察主体时执行的。 观察者调用 UnRegister 方法,并将其自身作为
参数进行传递。 然后,主体对容器调用 “移除” 方法以结束观察过程。
观察者------主体-----容器
回到我们的股票应用程序,让我们分析一下注册和通知过程所产生的影响。 在应用程序
启动过程中,一个 StockDisplay 类实例注册到 “常用” 实例中,并将其自身作为参数
传递到 Register 方法。 “常用” 实例(在容器中)保存对 StockDisplay 实例的引用
。 当询价属性发生变化时,“常用” 实例通过调用 “通知” 方法向 StockDisplay通
知所发生的变化。 在应用程序关闭时,StockDisplay 实例使用以下方法撤消注册“常用
” 实例:调用 UnRegister 方法,终止两个实例之间的关系。
请注意利用容器(而不是使用实例变量)来存储观察者引用有什么优点。 假定除当前用
户接口 StockDisplay 外,我们还需要绘制询价在交易日内变化的实时图形。 为此,我
们创建了一个名为 StockGraph 的新类,它绘制询价(y 轴)和当天时间(x 轴)的图形
。 在应用程序启动时,它同时在 “常用” 实例中注册 StockDisplay 和 StockGraph
类的实例。 因为主体在容器(与实例变量相对)中存储观察者,所以这不会出现问题。
当询价发生变化时,“常用” 实例向其容器中的两个 观察者实例通知所发生的状态变化
。 正如我们所看到的一样,使用容器可提供更大的灵活性,即每个主体可支持多个观察
者。 这使主体有可能向无数多个观察者通知所发生的状态变化,而不是只通知一个观察
者。
虽然这并不是一个要求,但很多框架为观察者和主体提供了一组要实现的接口。 正如下
面的 C# 和 Microsoft_ Visual Basic_ .NET 代码示例所示,IObserver 接口公开一种
公共方法 “通知”。 此接口是由所有要用作观察者的类实现的。 IObservable 接口(
是由所有要用作主体的类实现的)公开两种方法 Register 和 UnRegister。 这些接口通
常采用抽象虚拟类或真实接口的形式(如果实现语言支持此类构造的话)。 利用这些接
口有助于减少观察者和主体之间的耦合关系。 与观察者和主体类之间的紧密耦合关系不
同,IObserver 和 IObservable 接口允许执行独立于实现的操作。 通过对接口的分析,
您将注意到键入的所有方法针对的是接口类型(与具体类相对)。 这种方法将接口编程
模型的优点扩展到观察者模式。
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
IObserver 和 IObservable 接口 (Visual Basic .NET)
'interface the all observer classes should implement
Public Interface IObserver
Sub Notify(ByVal anObject As Object)
End Interface
'interface that all observable classes should implement
Public Interface IObservable
Sub Register(ByVal anObserver As IObserver)
Sub UnRegister(ByVal anObserver As IObserver)
End Interface
再回到我们的示例应用程序,我们知道 “常用” 类用作主体。 因此,它将实现
IObservable 接口。 类似地,StockDisplay 类实现 IObserver 接口。 因为所有操作都
是由该接口定义的(而不是由具体类定义的),所以 “常用” 类并未与 StockDisplay
类绑定在一起,反之亦然。 这使我们能够快速地更改特定的观察者或主体实现,而不会
影响应用程序的其他部分(使用不同的观察者替换 StockDisplay 或添加额外的观察者实
例)。
除了这些接口外,框架还经常为主体提供一个用于扩展的通用基类。 此基类扩展减少了
支持观察者模式所需的工作。 基类实现 IObservable 接口,以提供支持观察者实例存储
和通知所需的基础结构。 下面的 C# 和 Visual Basic .NET 代码示例简要介绍一个名为
ObservableImpl 的此类基类。 尽管可能任何容器都可以完成这一任务,但该类在
Register 和 UnRegister 方法中将观察者存储委派给哈希表实例(为了方便起见,我们
在示例中使用哈希表作为容器,它只使用一个方法调用来撤消注册特定的观察者实例)。
还要注意添加了 NotifyObservers 方法。 此方法用于通知哈希表中存储的观察者。 在
调用此方法时,将枚举该容器,并对观察者实例调用 “通知” 方法。
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
ObservableImpl 类 (Visual Basic .NET)
'helper class that implements observable interface
Public Class ObservableImpl
Implements IObservable
'container to store the observer instance (is not synchronized for this
example)
Dim _observerContainer As Hashtable = New Hashtable()
'add the observer
Public Sub Register(ByVal anObserver As IObserver) Implements
IObservable.Register
_observerContainer.Add(anObserver, anObserver)
End Sub
'remove the observer
Public Sub UnRegister(ByVal anObserver As IObserver) Implements
IObservable.UnRegister
_observerContainer.Remove(anObserver)
End Sub
'common method to notify all the observers
Public Sub NotifyObservers(ByVal anObject As Object)
Dim anObserver As IObserver
'enumerate the observers and invoke their notify method
For Each anObserver In _observerContainer.Keys
anObserver.Notify(anObject)
Next
End Sub
End Class
我们的示例应用程序使用以下方法来利用此基类基础结构:修改 “常用” 类以扩展
ObservableImpl 类,而不是提供其自己的特定 IObservable 接口实现。 因为
ObservableImpl 类实现了 IObservable 接口,所以不需要对 StockDisplay 类进行任何
更改。 实际上,这种方法简化了观察者模式的实现,在保持涉及的类之间的松散耦合关
系的同时,使多个主体重复使用相同的功能。
以 C# 和 Visual Basic 编写的观察者示例。 下面的 .NET 观察者示例重点说明了
IObservable 和 IObserver 接口以及 ObservableBase 类在我们的股票应用程序中的使
用情况。 除了 “常用” 和 StockDisplay 类外,此示例使用 MainClass 将观察者和主
体实例关联起来,并修改 “常用” 实例的 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
观察者示例 (Visual Basic .NET)
'Represents a stock in an application
Public Class Stock
Inherits ObservableImpl
'instance variable for ask price
Dim _askPrice As Object
'property for ask price
Public WriteOnly Property AskPrice()
Set(ByVal value As Object)
_askPrice = value
NotifyObservers(_askPrice)
End Set
End Property
End Class
'represents the user interface in the application
Public Class StockDisplay
Implements IObserver
Public Sub Notify(ByVal anObject As Object) Implements IObserver.Notify
Console.WriteLine("The new ask price is:" & anObject)
End Sub
End Class
Public Class MainClass
Shared Sub Main()
'create new grid and stock instances
Dim stockDisplay As StockDisplay = New StockDisplay()
Dim stock As Stock = New Stock()
Dim looper As Integer
'register the display
stock.Register(stockDisplay)
'loop 100 times and modify the ask price
For looper = 0 To 100
stock.AskPrice = looper
Next looper
'unregister the display
stock.UnRegister(stockDisplay)
End Sub
.NET 框架中的观察者模式
基于我们对观察者模式的了解,让我们将注意力转向此模式在 .NET 框架中的使用情况。
您们当中非常熟悉 FCL 中所公开类型的人将会注意到,框架中没有 IObserver、
IObservable 或 ObservableImpl 类型。 没有这些类型的主要原因是,在流行一段时间
后,CLR 将这些类型弃用。 虽然您的确可以在 .NET 应用程序中使用这些构造,但引入
委派和事件可提供新的、功能强大的方法来实现观察者模式,而不必开发专用于支持该模
式的特定类型。 事实上,因为委派和事件是 CLR 的一级成员,所以将此模式的基本构造
添加到 .NET 框架的核心中。 因此,FCL 在其结构中广泛使用观察者模式。
介绍委派和事件内部工作方式的文章非常多,我们在此不再赘述。 我们只需说明委派是
函数指针面向对象(和类型安全)的等效物就可以了。 委派实例保存对实例或类方法的
引用,允许匿名调用绑定方法。 事件是在类上声明的特殊构造,可帮助在运行时公开感
兴趣的对象的状态变化。 事件表示我们前面用于实现观察者模式的注册、撤消注册和通
知方法的形式抽象(CLR 和多种不同的编译器对它提供支持)。 委派是在运行时注册到
特定事件中的。 在引发事件时,将调用所有注册的委派,以使它们能够收到事件的通知
。
在观察者模式环境中介绍委派和事件之前,需要注意的是,CLR 支持的各种语言可自由公
开委派和事件机制,只要语言设计者认为合适即可。 因此,无法在不同的语言中综合地
研究这些功能。 为了便于以下讨论,我们将重点放在这些功能的 C# 和 Visual Basic
.NET 实现上。 如果您使用的语言不是 C# 或 Visual Basic .NET,请参阅相关文档,了
解有关在您的语言中如何支持委派和事件的更多信息。
按照观察者模式定义的术语,声明事件的类就是主体。 与我们以前使用的 IObservable
接口和 ObservableImpl 类不同,主体类不需要实现给定接口或扩展基类。 主体只需要
公开一个事件,而不需要执行任何其他操作。 观察者创建涉及的工作略多一些,但灵活
性却提高得非常多(我们将在后面讨论)。 观察者并不实现 IObserver 接口和将其自身
注册到主体中,而是必须创建特定的委派实例,并将此委派注册到主体事件中。 观察者
必须使用具有事件声明所指定类型的委派实例,否则,注册就会失败。 在创建此委派实
例的过程中,观察者将传递该主体向委派通知的方法(实例或静态)名称。 在将委派绑
定到方法后,可以将其注册到主体的事件中。 类似地,也可以从事件中撤消注册此委派
。 主体通过调用事件向观察者提供通知。
如果您不熟悉委派和事件,则实现观察者模式似乎需要做很多工作,尤其是与我们以前使
用的 IObserver 和 IObservable 接口相比。 但是,它比听起来要简单一些,并且实现
起来要容易得多。 下面的 C# 和 Visual Basic .NET 代码示例重点说明了在我们的示例
应用程序中支持委派和事件所需的类修改。 注意,没有 “常用” 或 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
使用委派和事件的观察者 (Visual Basic .NET)
'represents a stock in an application
Public Class Stock
'declare a delegate for the event
Delegate Sub AskPriceDelegate(ByVal aPrice As Object)
'declare the event using the delegate
Public Event AskPriceChanged As AskPriceDelegate
'instance variable for ask price
Dim _askPrice As Object
'property for ask price
Public WriteOnly Property AskPrice()
Set(ByVal value As Object)
_askPrice = value
RaiseEvent AskPriceChanged(_askPrice)
End Set
End Property
End Class
'represents the user interface in the application
Public Class StockDisplay
Public Sub Notify(ByVal anObject As Object)
Console.WriteLine("The new ask price is:" & anObject)
End Sub
End Class
Public Class MainClass
Shared Sub Main()
'create new display and stock instances
Dim stockDisplay As StockDisplay = New StockDisplay()
Dim stock As Stock = New Stock()
Dim looper As Integer
'register the delegate
AddHandler stock.AskPriceChanged, AddressOf stockDisplay.Notify
'loop 100 times and modify the ask price
For looper = 0 To 100
_stock.AskPrice = looper
Next looper
'unregister the delegate
RemoveHandler stock.AskPriceChanged, AddressOf stockDisplay.Notify
End Sub
在熟悉了委派和事件后,您就会清楚地看到它们的巨大潜力。 与 IObserver 和
IObservable 接口以及 ObservableImpl 类不同,使用委派和事件可大大减少实现此模式
所需的工作量。 CLR 和编译器为观察者容器管理提供了基础,并且为注册、撤消注册和
通知观察者提供了一个通用调用约定。 也许,委派的最大优点是其能够引用任何方法的
固有特性(条件是它符合相同的签名)。 这允许任何类用作观察者,而与它所实现的接
口或它专用的类无关。 虽然使用 IObserver 和 IObservable 接口可减少观察者和主体
类之间的耦合关系,但使用委派可完全消除这些耦合关系。
事件模式
基于事件和委派,FCL 可以非常广泛地使用观察者模式。 FCL 的设计者充分认识到此模
式的巨大潜力,并在整个框架中将其应用于用户界面和非 UI 特定的功能。 但是,用法
与基本观察者模式稍有不同,框架小组将其称为事件模式。 通常,将此模式表示为事件
通知进程中所涉及的委派、事件和相关方法的正式命名约定。 虽然 CLR 或标准编译器并
没有强制要求利用事件和委派的所有应用程序和框架都采用这种模式(提防模式警察!)
,但 Microsoft 建议这样做。
其中的第一条约定也可能是最重要的约定是主体公开的事件的名称。 对于它所表示的状
态变化而言,此名称应该是不证自明的。 切记,此约定以及所有其他此类约定本身就是
主观性的。 目的是为那些利用您的事件的人员提供清晰的说明。 事件模式的其他部分利
用正确的事件命名,因而此步骤对模式来说至关重要。
回到我们可靠的示例,让我们分析一下这种约定对 “常用” 类产生的影响。 派生事件
名称的适当方法是,利用在主体类中修改的字段的名称作为根。 因为在 “常用” 类中
修改的字段名称是 _askPrice,所以合理的事件名称应该是 AskPriceChanged。 很明显
,此事件的名称比 StateChangedInStockClass 等具有更大的说明性。 因此,
AskPriceChanged 事件名称符合第一条约定。
事件模式中的第二条约定是正确命名委派及其签名。 委派名称应该包含事件名称(通过
第一个约定选择的)及附加词 “处理程序”。 此模式要求委派指定两个参数,第一个参
数提供对事件发送方的引用,第二个参数向观察者提供环境信息。 第一个参数的名称就
是 “发件人”。 必须将此参数键入为 System.Object。 这是由于以下事实:可能将委
派绑定到系统中任何类上的任何潜在方法。 第二个参数的名称(甚至比第一个参数更简
单)为 e。 必须将此参数键入为 System.EventArgs 或某种派生类(有时比此内容还多
)。 虽然委派的返回类型取决于您的实现需要,但大多数实现此模式的委派根本不返回
任何值。
需要稍加注意委派的第二个参数 e。 此参数允许主体对象将任意环境信息传递给观察者
。 如果不需要此类信息,则使用 System.EventArgs 实例就足够了,因为此类的实例表
示没有环境数据。 否则,应该使用相应的实现构造从 System.EventArgs 派生的类以提
供此数据。 必须按照具有附加词 EventArgs 的事件名称来命名该类。
请参考我们的 “常用” 类,此约定要求将处理 AskPriceChanged 事件的委派命名为
AskPriceChangedHandler。 此外,应该将此委派的第二个参数命名为
AskPriceChangedEventArgs。 因为我们需要将新的询价传递给观察者,所以我们需要扩
展 System.EventArgs 类,以将该类命名为 AskPriceChangedEventArgs 并提供实现来支
持传递此数据。
事件模式中的最后一个约定是负责引发事件的主体类上方法的名称和可访问性。 此方法
的名称应该包含事件名称以及添加的 On 前缀。 应该将此方法的可访问性设置为保护。
此约定仅适用于非密封(在 VB 中不可继承)类,因为它作为派生类调用在基类中注册的
观察者的已知的调用点。
将此最后一条约定应用于 “常用” 类,即可完成事件模式。 因为 Stock 类不是密封的
,所以我们必须添加一种方法来引发事件。 按照该模式,此方法的名称为
OnAskPriceChanged。 下面的 C# 和 Visual Basic .NET 代码示例显示应用于 “常用”
类的事件模式的完整视图。 请注意我们的 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
事件模式示例 (Visual Basic .NET)
Public Class Stock
'declare a delegate for the event
Delegate Sub AskPriceChangedHandler(ByVal sender As Object, ByVal e As
AskPriceChangedEventArgs)
'declare the event using the delegate
Public Event AskPriceChanged As AskPriceChangedHandler
'instance variable for ask price
Dim _askPrice As Object
Public Property AskPrice() As Object
Get
AskPrice = _askPrice
End Get
Set(ByVal Value As Object)
_askPrice = Value
OnAskPriceChanged()
End Set
End Property
'method to fire event delegate with proper name
Protected Sub OnAskPriceChanged()
RaiseEvent AskPriceChanged(Me, New AskPriceChangedEventArgs
(_askPrice))
End Sub
End Class
Public Class AskPriceChangedEventArgs
Inherits EventArgs
'instance variable to store the ask price
Dim _askPrice As Object
Sub New(ByVal askPrice As Object)
_askPrice = askPrice
End Sub
Public ReadOnly Property AskPrice() As Object
Get
AskPrice = _askPrice
End Get
End Property
End Class
结论
基于本次对观察者模式的分析,我们可以清楚地看到此模式提供了一个完美的机制,确保
在应用程序中的对象之间划定清晰的界限,而无论它们的作用(UI 或其他)是什么。 虽
然通过回调进行实现(使用 IObserver 和 IObservable 接口)相当简单,但 CLR 的委
派和事件概念可处理大多数“繁重的工作”,并降低主体和观察者之间的耦合级别。 实
际上,通过正确地使用此模式,在确保应用程序能够演变方面就会向前迈出一大步。 当
您的 UI 和业务要求随时间发生变化时,观察者模式可确保能够简化您的工作。
在开发灵活的应用程序方面,设计模式是一个非常强大的工具(如果有效地加以运用)。
撰写本文是为了说明模式方法的有效性,并重点说明 .NET 框架中使用的一种模式。 将
来的文章将继续探究 FCL 中的模式,并简要介绍一些用于生成有效 Web 服务的模式。
到那时……
虽然看起来只进行几个步骤,但是,真正的过程常常更为复杂。 消息的传送通常要通过
中间层(例如,高速缓存、过滤防火墙和凭证管理系统)。 这些中间层可以说是要侦听
消息,以进行部分处理或完整处理。 中间层可能产生异常(例如,拒绝访问请求的授权
中间层),导致返回给发送者一个错误。
侦听是消息处理基础结构的概念基础。 通过侦听,消息得以沿着复杂的路径路由,以便
共享的专门服务可以参与消息处理。
侦听更常见的一个应用是,使用不同的消息格式在服务之间进行请求转换,如图 1 所示
。 这种模式既可以用于在使用专有协议的系统的前面提供符合标准的接口,又可用于转
换遵守过时版本的服务协定的消息。
请求也可以基于消息的任何元素或属性重定向到其他服务端口。 这种基于内容的路由的
示例包括 状态划分(例如,按日期划分的新闻存档)和网络拓扑优化(将请求路由到本
地服务端口,而不是相距许多跃点的服务端口)。
在服务 一文中已经简单讨论了异步消息传送的设计价值。 除非我们必须使请求非常迅速
地收到响应(例如,一个不耐烦的用户访问网页的请求),否则,服务应该使用成对的端
口进行异步交互。 发出请求的服务应该在消息头中提供响应可以被发送到的端口标识符
。 实际上,请求者被放在消息处理链的末端,使用由实现与请求相关联的基本业务逻辑
的中间层和服务创建的响应。
消息交换的形式
前面简单讨论了几种不同的消息交换形式。 不同的交换形式适用于不同的服务。 以下是
一些交换形式的示例以及它们的使用原理:
• Fire and forget(发后不理)。 使用这种形式时,只发送一个消息,并且不 期望得
到(或者不愿意接受)响应。 这种形式通常用于发送状态更新,如温度读数。
• Monolog(独白)。 使用这种形式时,消息流被推向服务端口,没有应答。 独白通常
用于音频/可视内容,经常使用多播或广播将内容推向一个以上的接收者。
• “请求/响应”(请求/响应)。 这是一种我们很熟悉的形式,此时,客户端 期望立即
得到对信息请求的响应。 这是 HTTP 在 Web 上使用的主要消息交换形式。
• “对话框”(对话)。 这是一种与多个由认可的协议绑定的相关交换建立的 点到点连
接。 简单邮件传输协议 (SMTP) 就采用这种形式。
• Conversations(会话)。 虽然所有前面的形式都可以视为“会话”形式, 但该术语
在这里用于描述一种可能涉及许多服务的灵活的交换。 利用会话形式,可以进行支持实
际业务功能所必不可少的复杂交换。
提供这个信息交换形式清单的目的并不是建立一个词汇表,而是为了说明服务设计人员需
要选择适合应用要求的交换形式。
可以在服务提供中混合使用这些形式。 启动“独白”的请求/响应交换就是一个示例(例
如,请求新闻提供的新闻“自动收录器”应用程序)。 该自动收录器会发送一个包含将
接受消息流的端口标识符的请求。 新闻提供服务会验证请求,如果该请求有当前订户的
凭证, 就会作出肯定的响应。 新闻提供服务会将自动收录器的端口添加到多路广播列表
,之后,自动收录器就开始接收消息。
长时间运行的异步会话给消息处理带来了若干个复杂因素。 首先,涉及到的一些服务,
尤其是过程的发起方,需要维护有关会话的某种状态。 例如,供应管理服务可能代表技
术人员触发采购请求。 为了能够向技术人员通知进度(即使是告知正在进行中),需要
在交换的所有消息中都包括一个标记,以便唯一地标识会话。 其次,在会话中,必须以
每个消息为基础来建立安全上下文;没有任何会话上下文可用于凭据的缓存。
消息处理要求
与对网页的请求不同,实现高价值业务流程的服务通常更关心传送机制的可靠性,而不是
响应的速度。 为了使基于服务的应用程序结构成为业务应用程序值得信赖的平台,必须
满足一些使用要求,这部分内容将简单介绍其中的一些要求。
可靠的消息处理
同步消息传送不可能完全可靠。 由于网络问题或系统故障,目标端口可能不可用。 网络
延迟可能导致无法预测的请求滞后时间。 由于路由特性,消息流可能以错误的顺序到达
。
传输的不可靠性的一般解决方案是,如果最初的传送尝试失败,就将请求排队并依靠重传
。 但这个过程又带来了另一个可能发生的问题: 同一个消息的多个回执可能产生很不好
的效果(例如,进行重复的订购)。 消息传送的一个原则是,确保消息的幂等性,也就
是说,确保一个消息的多个回执具有与一个回执相同的效果。
可靠的消息传送基础结构的目标是,确保消息只按照正确顺序传送一次,在由策略驱动的
时间间隔内无法实现此目标时,减少异常的产生。
并非所有应用程序都需要可靠的消息传送;例如,如果丢失某个消息,包括丢弃“迟到”
的消息(也就是说,在后续消息之后到达),流音频也可以正常播放。 但即使在这种情
况下,应用程序也需要具有消息排序的意识,这样才能“播放”以错误顺序到达的消息内
容。
针对可靠消息处理的大部分支持已经包含在基础结构服务中,无需为每个应用程序重新编
写。 但在副作用特别严重时,可能需要对服务进行特殊改进,以确保消息的幂等性。
路由
复杂的消息路由对于实施实际的解决方案是必不可少的。 消息可能需要通过在许多其他
解决方案中提供可靠的消息传送、检查安全凭据以及维护消息通信的独立审核跟踪的服务
来进行路由。
为了使不同的服务都能理解各个消息的路由需求,必须有一定的标准。 当 A 成功完成消
息的处理后,消息头需要能够告诉服务 A:消息应该被发送到服务 B。 消息处理链中的
所有服务都需要知道出现意外时如何路由异常。
如果使用了许多不兼容的路由协议规范,结果将产生一堆非可互操作的服务。 服务架构
师应该选择满足最低应用程序要求的、实现最广泛的规范。
安全管理
安全性是在将业务流程移植到可由组织之外的其他方访问的网络时需要考虑的主要问题。
消息需要得到保护,防止发生数据盗取和篡改;人员和系统需要经过可靠的身份验证;服
务必须针对服务攻击的入侵以及如何拒绝攻击而进行强化。
网络安全是一个需要大量特定解决方案的多方面的问题。 有些安全机制可作为共享服务
或通过路由和过滤基础结构得到最佳实现,而其他方面的安全问题则必须在服务本身的范
围内被解决。
软件设计人员的考虑因素包括:
• 编译器和运行时环境的选择。 如果超出界限的内存访问可以用来影响外来代码的执行
,服务就很容易受到危害。 目前的开发工具和执行环境有助于防止这种攻击。
• 日志记录和日志分析。 服务应建模为理解正常的访问模式和异常的访问模式。 所开发
的系统应该能够向操作人员警告可疑活动。 服务访问模型会随着时间的推移不断发展,
因为实际操作经验有助于加深组织的理解。
• 数据加密。 应该使用机制来保护敏感数据,防止未经授权的其他方对其进行访问(包
括被授权作用于消息的其他部分的中间服务)。 可以通过用只有预期的参与者才知道如
何解密的方法对消息的特定部分进行加密来实现此目的。
• 消息完整性。 安全校验和可用于说明消息在传输过程中尚未被修改。 它可以应用于部
分消息,也可以应用于整个消息。 因为中间层可能需要向消息添加头元素,所以,整个
消息的校验和可能需要在每一跳重新计算;这个过程有力地支持了基于标准的完整性检查
方法。
• 身份验证和授权。 识别远程用户和服务带来了相当大的挑战。 用户简直就是运行方面
一个最令人头疼的问题:当合作伙伴组织中发生角色变更时,将影响对您的服务的访问权
限;而从不同提供商访问服务的用户却不想管理每位用户的唯一凭据。 解决方案是,将
身份验证和权限管理安全地委托给合作伙伴,同时,服务协定明确地规定由合作伙伴对来
自其组织的不当访问负责。 在不久的将来,这将是一个相当大的创新和开发领域。
记录和审核
出于对组织智能以及对前面提到的安全注意事项的考虑,组织必须理解服务的使用原理。
共享的日志记录功能应该向共享的分析引擎提供信息,分析引擎可允许组织进行服务建模
、规划容量并对产生的问题进行故障排除。
有些服务可能需要由服务使用者和服务提供商以外的独立代理提供的可审核记录。 使用
这样的服务,可能对遵守政府规定或解决在合作伙伴组织之间就有关处理了哪些消息以及
何时处理所可能发生的争端是非常必要的。
缓存管理
有几类服务会产生可缓存的结果。 其中可能包括静态信息,例如,证券在特定日期的收
盘价格,或可以在一段时间内有效地对其进行处理、但在采取行动之前应该进行验证的数
据(例如,飞机航班上座位的售出情况)。
Web 缓存使用统一资源定位符 (URL) 作为密钥,并且很少尝试缓存复杂查询的结果。 服
务不提供这样的简单方法,因此必须通过开发标准来为服务请求派生密钥,并允许服务在
响应中向中间层和使用者告知关于数据可缓存性的情况。
第 6 章“State”将详细讨论服务如何使用可缓存的数据以及不可缓存的数据。
消息处理基础结构
在刚刚提出的对强健的消息处理的操作要求中,有许多应该在消息处理基础结构中实现。
该基础结构在概念上包括:
• 公用组件。 组织需要为所有实施服务的系统使用的侦听和消息处理软件设立标准。
• 组织基础结构。 路由器、防火墙和共享服务在满足安全要求、日志记录要求和基于内
容的路由管理要求方面扮演了重要角色。
• 合作伙伴基础结构。 类似地,使用您的服务的组织的网络和服务基础结构对于服务交
付的安全性和可靠性同样至关重要。
• 公共基础结构。 消息可能在使用组织和提供组织的网络的边缘的路由器之间经过很长
的路径。 除了提供了简单的比特流,公共基础结构还包括组织选择的用作实现审核、身
份验证、加速和其他功能的中间层的服务。
图 2 以图形的方式描述了消息处理基础结构。
由于所有消息都要通过消息处理基础结构,因此,它是管理操作功能的理想环境。 这就
意味着,您可以将核心业务功能(即您的服务提供的业务流程)与操作功能分开,这是您
的服务与其他服务通信的方式。 通过这些功能的分离,还可以将责任分配给组织内适当
的专家组。
虽然您是独立于服务将使用的传输来设计服务的,但仍须考虑到这些服务还是会涉及到网
络传输的。 业务逻辑将需要照顾分布式处理和异步处理的复杂性。
消息处理小结
消息是网络服务的设计中心。 通过将重点放在正被操作的状态的线格式,基于服务的结
构有利于集成和互操作性的设计。 针对水平消息处理问题的标准解决方案将极大地有助
于实现可互操作的服务的目标。
消息沿着复杂的路线,从使用者那里出发,通过中间层,到达服务提供商。 最终的响应
也可能沿着另一个同等复杂的路径返回。 服务和使用服务的应用程序必须在考虑到这些
复杂性的前提下进行设计和开发。 将服务调用视为同步功能调用会产生脆弱的应用程序
。
编写服务以及使用服务的应用程序时,涉及的大多数复杂因素应该被提升到在应用程序要
求和组织策略允许的前提下尽可能更广泛地共享的消息处理基础结构。 此基础结构将成
为未来几年内整个计算机行业中创新和开发的关键方面。