UML软件工程组织

探究观察者设计模式
来源:www.microsoft.com

摘要:

本文讨论在 Microsoft .NET 框架中使用的设计模式。 开发人员或架构师可以在开发项目中使用设计模式这种非常强大的工具,设计模式可确保用已知 和公认的解决方案处理常见问题,更迅速地开发正确的代码,以降低设计或实现过程中出错的几率。

内容

本页内容
简介 简介
为什么要使用设计模式? 为什么要使用设计模式?
观察者模式 观察者模式
结论 结论
消息交换的形式 消息交换的形式

				>消息处理要求
			消息处理要求
消息处理基础结构 消息处理基础结构
消息处理小结 消息处理小结
注意 注意
简介

在开发给定项目的过程中,通常会使用设计模式概念来解决与应用程序设计和结构有关的某些问题。 但是,设计模式定义通常很难准确地表达信息;因此,此概念保证了对起源和历史情况提供简短的说明。

软件设计模式的起源归因于 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 种设计模式,这些模式分析了在设计和构造应用程序时遇到的常见问题和解决方案。 在这个出版物之后,设计模式的概念已发展为包含在软件领域中遇到的很多 问题和解决方案。 事实上,设计模式的广泛采用导致了反模式概念的出现,这些模式是指通常会使手头问题更加严重而不加解决问题的解决方案。

为什么要使用设计模式?

虽然设计模式并不是万能钥匙(如果世上真有此物的话),但它是一个非常强大的工具,开发人员或架构师可使用它积极地参与任何开发项目。 设计模式可确保通过熟知和公认的解决方案解决常见问题。 模式存在的事实基础在于:大多数问题其他个人或开发小组可能已经遇到并解决了。 因此,模式提供了一种在开发人员和组织之间共享可使用的解决方案的机制。 无论这些模式的出处是什么,这些模式都利用了大家所积累的知识和经验。 这可确保更快地开发正确的代码,并降低在设计或实现中出现错误的可能性。 此外,设计模式在工程小组成员之间提供了通用的语义。 参加过大型开发项目的人员都知道,使用一组共同的设计术语和准则对成功完成项目来说是至关重要的。 最重要的是,设计模式可以节省您大量的时间 (如果正确使用的话)。

观察者模式

虽然 GoF 将他们的示例限制在 C++ 和 Smalltalk,但设计模式并不专门与给定语言或开发平台捆绑在一起;Microsoft .NET 框架的出现为分析设计模式提供了新的机会和环境。 在框架类库 (FCL) 的开发过程中,Microsoft 应用了很多 GoF 于 1994 年首次提出的相同模式。由于 .NET 框架中提供的功能范围非常广泛,因此,还开发和提出了一些全新的模式。

在本系列文章中,我们将详细介绍 FCL 中涉及的一些设计模式。 我们将考虑每种模式的一般结构和优点,然后介绍 FCL 中的具体实现情况。 虽然我们介绍的大多数模式出自 GoF,但是,.NET 框架提供了很多新颖的功能,目前尚没有或几乎没有相应的设计原则。 我们还将介绍与这些新功能相关的设计模式。 我们对设计模式的研究从观察者模式入手。

观察者模式

面向对象的开发的一个主导原则是,在给定的应用程序中正确地分配任务。 系统中的每个对象应该将重点放在问题域中的离散抽象上,而不是放在任何其它方面。 简而言之,一个对象只应做一件事,而且要将它做好。 这种方法可确保在对象之间划定清晰的界限,因而可提供更高的重用性和系统可维护性。

一个正确划分任务特别重要的领域就是,用户界面和基础业务逻辑之间的交互。 在应用程序的开发过程中,需要快速更改用户界面要求,并且不会对应用程序的其他部分产生连带影响,这是司空见惯的事。 此外,业务要求也可能会发生变化,而这一切与用户界面无关。 具有丰富开发经验的人都知道,在很多情况下,这两组要求都会发生变化。 如果没有划分 UI 和应用程序其他部分,修改任一部分都会对整体造成不利的影响。

很多应用程序都会遇到以下常见问题:需要在用户界面和业务逻辑之间划分清晰的界限。 因此,自 GUI 出现以后开发的很多面向对象的框架均支持将用户界面从应用程序的其他部分中划分出来。 不要惊讶(可能有一点),其中的大部分应用程序采用类似的设计模式来提供此功能。 这种模式通常称为观察者,它在系统中的各种对象之间划分清晰的界限方面非常有利。 此外,还会经常看到在框架或应用程序中与 UI 无关的部分中使用这种解决方案。 正如大多数其他模式一样,观察者模式的作用远远超过了其最初的想法。

逻辑模型

虽然观察者模式有很多变体,但该模式的基本前提包含两个角色:观察者主体(熟悉 Smalltalk MVC 的人将这些术语分别称为视图和模型)。 在用户界面的环境中,观察者是负责向用户显示数据的对象。 另一方面,主体表示从问题域中模拟的业务抽象。 正如图 1 中所描述的一样,在观察者和主体之间存在逻辑关联。 当主体对象中发生更改时,(例如,修改实例变量),观察者就会观察 这种更改,并相应地更新其显示。


图 1. 观察者和主体关系

例如,假定我们要开发一种简单的应用程序,来跟踪全天的股票价格。 在此应用程序中,我们指定一个 “常用” 类来模拟在 NASDAQ 交易的各种股票。 该类包含一个实例变量,它表示在全天不同时段经常波动的当前询价。 为了向用户显示此信息,应用程序使用一个 StockDisplay 类向 stdout(标准输出)写入信息。 在应用程序中,一个 “常用” 类实例作为主体,一个 StockDisplay 类实例作为观察者。 随着询价在交易日中随时间发生变化,“常用” 实例的当前询价也会发生变化(它怎样变化并不重要)。 因为 StockDisplay 实例正在观察“常用” 实例,所以在这些状态发生变化(修改询价)时,就会向用户显示这些变化。

通过使用这种观察过程,可确保在 “常用”StockDisplay 类之间划分界限。 假定应用程序的要求第二天发生变化,因而要求使用基于窗体的用户界面。 要启用此新功能,只需要构造一个新类 StockForm 作为观察者。 无论发生什么情况,“常用” 类都不需要进行任何修改。 事实上,它甚至不知道发生此类更改。 类似地,如果需求变化要求 “常用” 类从另一个来源检索询价信息(可能是从 Web 服务,而不是从数据库中检索),则 StockDisplay 类不需要进行修改。 它只是继续观察 “常用”,而并不注意发生的任何变化。

物理模型

正如大多数解决方案一样,难题出在细节上。 观察者模式也不例外。 虽然逻辑模型规定观察者观察主体;但实现这种模式时,这实际上是一个名称误用。 更准确地说,观察者注册主体,表明它对观察的意向。 在某种状态发生变化时,主体向观察者通知这种变化情况。 当观察者不再希望观察主体时,观察者从主体中撤消注册。 这些步骤分别称为观察者注册通知撤消注册

大多数框架通过回调来实现注册和通知。 图 2、3 和 4 中所示的 UML 序列图模拟这种方法通常使用的对象和方法调用。 对于不熟悉序列图的人来说,最上面的矩形框表示对象,而箭头表示方法调用。


图 2. 观察者注册

图 2 描述了注册序列。 观察者对主体调用 Register 方法,以将其自身作为参数传递。 在主体收到此引用后,它必须将其存储起来,以便在将来某个时间状态发生变化时通知观察者。 大多数观察者实现并非将观察者引用直接存储在实例变量中,而是将此任务委派给一个单独的对象(通常为一个容器)。 使用容器来存储观察者实例可提供非常大的好处,我们将对它进行简要介绍。 谨记这一点后,我们看序列中的下一个操作是存储观察者引用(通过在该容器上调用 Add 方法来指示)。


图 3. 观察者通知

图 3 突出显示了通知序列。 当状态发生变化时 (AskPriceChanged),主体通过调用 GetObservers 方法来检索容器中的所有观察者。 主体然后枚举检索的观察者,并调用 “通知” 方法以通知观察者所发生的状态变化。


图 4. 观察者撤消注册

图 4 显示撤消注册序列。 此序列是在观察者不再需要观察主体时执行的。 观察者调用 UnRegister 方法,并将其自身作为参数进行传递。 然后,主体对容器调用 “移除” 方法以结束观察过程。

回到我们的股票应用程序,让我们分析一下注册和通知过程所产生的影响。 在应用程序启动过程中,一个 StockDisplay 类实例注册“常用” 实例中,并将其自身作为参数传递到 Register 方法。 “常用” 实例(在容器中)保存对 StockDisplay 实例的引用。 当询价属性发生变化时,“常用” 实例通过调用 “通知” 方法向 StockDisplay通知所发生的变化。 在应用程序关闭时,StockDisplay 实例使用以下方法撤消注册“常用” 实例:调用 UnRegister 方法,终止两个实例之间的关系。

请注意利用容器(而不是使用实例变量)来存储观察者引用有什么优点。 假定除当前用户接口 StockDisplay 外,我们还需要绘制询价在交易日内变化的实时图形。 为此,我们创建了一个名为 StockGraph 的新类,它绘制询价(y 轴)和当天时间(x 轴)的图形。 在应用程序启动时,它同时“常用” 实例中注册 StockDisplayStockGraph 类的实例。 因为主体在容器(与实例变量相对)中存储观察者,所以这不会出现问题。 当询价发生变化时,“常用” 实例向其容器中的两个 观察者实例通知所发生的状态变化。 正如我们所看到的一样,使用容器可提供更大的灵活性,即每个主体可支持多个观察者。 这使主体有可能向无数多个观察者通知所发生的状态变化,而不是只通知一个观察者。

虽然这并不是一个要求,但很多框架为观察者和主体提供了一组要实现的接口。 正如下面的 C# 和 Microsoft_ Visual Basic_ .NET 代码示例所示,IObserver 接口公开一种公共方法 “通知”。 此接口是由所有要用作观察者的类实现的。 IObservable 接口(是由所有要用作主体的类实现的)公开两种方法 RegisterUnRegister。 这些接口通常采用抽象虚拟类或真实接口的形式(如果实现语言支持此类构造的话)。 利用这些接口有助于减少观察者和主体之间的耦合关系。 与观察者和主体类之间的紧密耦合关系不同,IObserverIObservable 接口允许执行独立于实现的操作。 通过对接口的分析,您将注意到键入的所有方法针对的是接口类型(与具体类相对)。 这种方法将接口编程模型的优点扩展到观察者模式。

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 的此类基类。 尽管可能任何容器都可以完成这一任务,但该类在 RegisterUnRegister 方法中将观察者存储委派给哈希表实例(为了方便起见,我们在示例中使用哈希表作为容器,它只使用一个方法调用来撤消注册特定的观察者实例)。 还要注意添加了 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 观察者示例重点说明了 IObservableIObserver 接口以及 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 中所公开类型的人将会注意到,框架中没有 IObserverIObservableObservableImpl 类型。 没有这些类型的主要原因是,在流行一段时间后,CLR 将这些类型弃用。 虽然您的确可以在 .NET 应用程序中使用这些构造,但引入委派事件可提供新的、功能强大的方法来实现观察者模式,而不必开发专用于支持该模式的特定类型。 事实上,因为委派和事件是 CLR 的一级成员,所以将此模式的基本构造添加到 .NET 框架的核心中。 因此,FCL 在其结构中广泛使用观察者模式。

介绍委派和事件内部工作方式的文章非常多,我们在此不再赘述。 我们只需说明委派是函数指针面向对象(和类型安全)的等效物就可以了。 委派实例保存对实例或类方法的引用,允许匿名调用绑定方法。 事件是在类上声明的特殊构造,可帮助在运行时公开感兴趣的对象的状态变化。 事件表示我们前面用于实现观察者模式的注册、撤消注册和通知方法的形式抽象(CLR 和多种不同的编译器对它提供支持)。 委派是在运行时注册到特定事件中的。 在引发事件时,将调用所有注册的委派,以使它们能够收到事件的通知。

在观察者模式环境中介绍委派和事件之前,需要注意的是,CLR 支持的各种语言可自由公开委派和事件机制,只要语言设计者认为合适即可。 因此,无法在不同的语言中综合地研究这些功能。 为了便于以下讨论,我们将重点放在这些功能的 C# 和 Visual Basic .NET 实现上。 如果您使用的语言不是 C# 或 Visual Basic .NET,请参阅相关文档,了解有关在您的语言中如何支持委派和事件的更多信息。

按照观察者模式定义的术语,声明事件的类就是主体。 与我们以前使用的 IObservable 接口和 ObservableImpl 类不同,主体类不需要实现给定接口或扩展基类。 主体只需要公开一个事件,而不需要执行任何其他操作。 观察者创建涉及的工作略多一些,但灵活性却提高得非常多(我们将在后面讨论)。 观察者并不实现 IObserver 接口和将其自身注册到主体中,而是必须创建特定的委派实例,并将此委派注册到主体事件中。 观察者必须使用具有事件声明所指定类型的委派实例,否则,注册就会失败。 在创建此委派实例的过程中,观察者将传递该主体向委派通知的方法(实例或静态)名称。 在将委派绑定到方法后,可以将其注册到主体的事件中。 类似地,也可以从事件中撤消注册此委派。 主体通过调用事件向观察者提供通知

如果您不熟悉委派和事件,则实现观察者模式似乎需要做很多工作,尤其是与我们以前使用的 IObserverIObservable 接口相比。 但是,它比听起来要简单一些,并且实现起来要容易得多。 下面的 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

 

在熟悉了委派和事件后,您就会清楚地看到它们的巨大潜力。 与 IObserverIObservable 接口以及 ObservableImpl 类不同,使用委派和事件可大大减少实现此模式所需的工作量。 CLR 和编译器为观察者容器管理提供了基础,并且为注册、撤消注册和通知观察者提供了一个通用调用约定。 也许,委派的最大优点是其能够引用任何方法的固有特性(条件是它符合相同的签名)。 这允许任何类用作观察者,而与它所实现的接口或它专用的类无关。 虽然使用 IObserverIObservable 接口可减少观察者和主体类之间的耦合关系,但使用委派可完全消除这些耦合关系。

事件模式

基于事件和委派,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 或其他)是什么。 虽然通过回调进行实现(使用 IObserverIObservable 接口)相当简单,但 CLR 的委派和事件概念可处理大多数“繁重的工作”,并降低主体和观察者之间的耦合级别。 实际上,通过正确地使用此模式,在确保应用程序能够演变方面就会向前迈出一大步。 当您的 UI 和业务要求随时间发生变化时,观察者模式可确保能够简化您的工作。

在开发灵活的应用程序方面,设计模式是一个非常强大的工具(如果有效地加以运用)。 撰写本文是为了说明模式方法的有效性,并重点说明 .NET 框架中使用的一种模式。 将来的文章将继续探究 FCL 中的模式,并简要介绍一些用于生成有效 Web 服务的模式。 到那时……

虽然看起来只进行几个步骤,但是,真正的过程常常更为复杂。 消息的传送通常要通过中间层(例如,高速缓存、过滤防火墙和凭证管理系统)。 这些中间层可以说是要侦听消息,以进行部分处理或完整处理。 中间层可能产生异常(例如,拒绝访问请求的授权中间层),导致返回给发送者一个错误。

侦听是消息处理基础结构的概念基础。 通过侦听,消息得以沿着复杂的路径路由,以便共享的专门服务可以参与消息处理。

侦听更常见的一个应用是,使用不同的消息格式在服务之间进行请求转换,如图 1 所示。 这种模式既可以用于在使用专有协议的系统的前面提供符合标准的接口,又可用于转换遵守过时版本的服务协定的消息。

请求也可以基于消息的任何元素或属性重定向到其他服务端口。 这种基于内容的路由的示例包括 状态划分(例如,按日期划分的新闻存档)和网络拓扑优化(将请求路由到本地服务端口,而不是相距许多跃点的服务端口)。

在服务 一文中已经简单讨论了异步消息传送的设计价值。 除非我们必须使请求非常迅速地收到响应(例如,一个不耐烦的用户访问网页的请求),否则,服务应该使用成对的端口进行异步交互。 发出请求的服务应该在消息头中提供响应可以被发送到的端口标识符。 实际上,请求者被放在消息处理链的末端,使用由实现与请求相关联的基本业务逻辑的中间层和服务创建的响应。

消息交换的形式

前面简单讨论了几种不同的消息交换形式。 不同的交换形式适用于不同的服务。 以下是一些交换形式的示例以及它们的使用原理:

Fire and forget(发后不理)。 使用这种形式时,只发送一个消息,并且不 期望得到(或者不愿意接受)响应。 这种形式通常用于发送状态更新,如温度读数。

Monolog(独白)。 使用这种形式时,消息流被推向服务端口,没有应答。 独白通常用于音频/可视内容,经常使用多播或广播将内容推向一个以上的接收者。

“请求/响应”(请求/响应)。 这是一种我们很熟悉的形式,此时,客户端 期望立即得到对信息请求的响应。 这是 HTTP 在 Web 上使用的主要消息交换形式。

“对话框”(对话)。 这是一种与多个由认可的协议绑定的相关交换建立的 点到点连接。 简单邮件传输协议 (SMTP) 就采用这种形式。

Conversations(会话)。 虽然所有前面的形式都可以视为“会话”形式, 但该术语在这里用于描述一种可能涉及许多服务的灵活的交换。 利用会话形式,可以进行支持实际业务功能所必不可少的复杂交换。

提供这个信息交换形式清单的目的并不是建立一个词汇表,而是为了说明服务设计人员需要选择适合应用要求的交换形式。

可以在服务提供中混合使用这些形式。 启动“独白”的请求/响应交换就是一个示例(例如,请求新闻提供的新闻“自动收录器”应用程序)。 该自动收录器会发送一个包含将接受消息流的端口标识符的请求。 新闻提供服务会验证请求,如果该请求有当前订户的凭证, 就会作出肯定的响应。 新闻提供服务会将自动收录器的端口添加到多路广播列表,之后,自动收录器就开始接收消息。

长时间运行的异步会话给消息处理带来了若干个复杂因素。 首先,涉及到的一些服务,尤其是过程的发起方,需要维护有关会话的某种状态。 例如,供应管理服务可能代表技术人员触发采购请求。 为了能够向技术人员通知进度(即使是告知正在进行中),需要在交换的所有消息中都包括一个标记,以便唯一地标识会话。 其次,在会话中,必须以每个消息为基础来建立安全上下文;没有任何会话上下文可用于凭据的缓存。

消息处理要求

与对网页的请求不同,实现高价值业务流程的服务通常更关心传送机制的可靠性,而不是响应的速度。 为了使基于服务的应用程序结构成为业务应用程序值得信赖的平台,必须满足一些使用要求,这部分内容将简单介绍其中的一些要求。

可靠的消息处理

同步消息传送不可能完全可靠。 由于网络问题或系统故障,目标端口可能不可用。 网络延迟可能导致无法预测的请求滞后时间。 由于路由特性,消息流可能以错误的顺序到达。

传输的不可靠性的一般解决方案是,如果最初的传送尝试失败,就将请求排队并依靠重传。 但这个过程又带来了另一个可能发生的问题: 同一个消息的多个回执可能产生很不好的效果(例如,进行重复的订购)。 消息传送的一个原则是,确保消息的幂等性,也就是说,确保一个消息的多个回执具有与一个回执相同的效果。

可靠的消息传送基础结构的目标是,确保消息只按照正确顺序传送一次,在由策略驱动的时间间隔内无法实现此目标时,减少异常的产生。

并非所有应用程序都需要可靠的消息传送;例如,如果丢失某个消息,包括丢弃“迟到”的消息(也就是说,在后续消息之后到达),流音频也可以正常播放。 但即使在这种情况下,应用程序也需要具有消息排序的意识,这样才能“播放”以错误顺序到达的消息内容。

针对可靠消息处理的大部分支持已经包含在基础结构服务中,无需为每个应用程序重新编写。 但在副作用特别严重时,可能需要对服务进行特殊改进,以确保消息的幂等性。

路由

复杂的消息路由对于实施实际的解决方案是必不可少的。 消息可能需要通过在许多其他解决方案中提供可靠的消息传送、检查安全凭据以及维护消息通信的独立审核跟踪的服务来进行路由。

为了使不同的服务都能理解各个消息的路由需求,必须有一定的标准。 当 A 成功完成消息的处理后,消息头需要能够告诉服务 A:消息应该被发送到服务 B。 消息处理链中的所有服务都需要知道出现意外时如何路由异常。

如果使用了许多不兼容的路由协议规范,结果将产生一堆非可互操作的服务。 服务架构师应该选择满足最低应用程序要求的、实现最广泛的规范。

安全管理

安全性是在将业务流程移植到可由组织之外的其他方访问的网络时需要考虑的主要问题。 消息需要得到保护,防止发生数据盗取和篡改;人员和系统需要经过可靠的身份验证;服务必须针对服务攻击的入侵以及如何拒绝攻击而进行强化。

网络安全是一个需要大量特定解决方案的多方面的问题。 有些安全机制可作为共享服务或通过路由和过滤基础结构得到最佳实现,而其他方面的安全问题则必须在服务本身的范围内被解决。

软件设计人员的考虑因素包括:

编译器和运行时环境的选择。 如果超出界限的内存访问可以用来影响外来代码的执行,服务就很容易受到危害。 目前的开发工具和执行环境有助于防止这种攻击。

日志记录和日志分析。 服务应建模为理解正常的访问模式和异常的访问模式。 所开发的系统应该能够向操作人员警告可疑活动。 服务访问模型会随着时间的推移不断发展,因为实际操作经验有助于加深组织的理解。

数据加密。 应该使用机制来保护敏感数据,防止未经授权的其他方对其进行访问(包括被授权作用于消息的其他部分的中间服务)。 可以通过用只有预期的参与者才知道如何解密的方法对消息的特定部分进行加密来实现此目的。

消息完整性。 安全校验和可用于说明消息在传输过程中尚未被修改。 它可以应用于部分消息,也可以应用于整个消息。 因为中间层可能需要向消息添加头元素,所以,整个消息的校验和可能需要在每一跳重新计算;这个过程有力地支持了基于标准的完整性检查方法。

身份验证和授权。 识别远程用户和服务带来了相当大的挑战。 用户简直就是运行方面一个最令人头疼的问题:当合作伙伴组织中发生角色变更时,将影响对您的服务的访问权限;而从不同提供商访问服务的用户却不想管理每位用户的唯一凭据。 解决方案是,将身份验证和权限管理安全地委托给合作伙伴,同时,服务协定明确地规定由合作伙伴对来自其组织的不当访问负责。 在不久的将来,这将是一个相当大的创新和开发领域。

记录和审核

出于对组织智能以及对前面提到的安全注意事项的考虑,组织必须理解服务的使用原理。 共享的日志记录功能应该向共享的分析引擎提供信息,分析引擎可允许组织进行服务建模、规划容量并对产生的问题进行故障排除。

有些服务可能需要由服务使用者和服务提供商以外的独立代理提供的可审核记录。 使用这样的服务,可能对遵守政府规定或解决在合作伙伴组织之间就有关处理了哪些消息以及何时处理所可能发生的争端是非常必要的。

缓存管理

有几类服务会产生可缓存的结果。 其中可能包括静态信息,例如,证券在特定日期的收盘价格,或可以在一段时间内有效地对其进行处理、但在采取行动之前应该进行验证的数据(例如,飞机航班上座位的售出情况)。

Web 缓存使用统一资源定位符 (URL) 作为密钥,并且很少尝试缓存复杂查询的结果。 服务不提供这样的简单方法,因此必须通过开发标准来为服务请求派生密钥,并允许服务在响应中向中间层和使用者告知关于数据可缓存性的情况。

第 6 章“State”将详细讨论服务如何使用可缓存的数据以及不可缓存的数据。

消息处理基础结构

在刚刚提出的对强健的消息处理的操作要求中,有许多应该在消息处理基础结构中实现。 该基础结构在概念上包括:

公用组件。 组织需要为所有实施服务的系统使用的侦听和消息处理软件设立标准。

组织基础结构。 路由器、防火墙和共享服务在满足安全要求、日志记录要求和基于内容的路由管理要求方面扮演了重要角色。

合作伙伴基础结构。 类似地,使用您的服务的组织的网络和服务基础结构对于服务交付的安全性和可靠性同样至关重要。

公共基础结构。 消息可能在使用组织和提供组织的网络的边缘的路由器之间经过很长的路径。 除了提供了简单的比特流,公共基础结构还包括组织选择的用作实现审核、身份验证、加速和其他功能的中间层的服务。

图 2 以图形的方式描述了消息处理基础结构。

由于所有消息都要通过消息处理基础结构,因此,它是管理操作功能的理想环境。 这就意味着,您可以将核心业务功能(即您的服务提供的业务流程)与操作功能分开,这是您的服务与其他服务通信的方式。 通过这些功能的分离,还可以将责任分配给组织内适当的专家组。

虽然您是独立于服务将使用的传输来设计服务的,但仍须考虑到这些服务还是会涉及到网络传输的。 业务逻辑将需要照顾分布式处理和异步处理的复杂性。

消息处理小结

消息是网络服务的设计中心。 通过将重点放在正被操作的状态的线格式,基于服务的结构有利于集成和互操作性的设计。 针对水平消息处理问题的标准解决方案将极大地有助于实现可互操作的服务的目标。

消息沿着复杂的路线,从使用者那里出发,通过中间层,到达服务提供商。 最终的响应也可能沿着另一个同等复杂的路径返回。 服务和使用服务的应用程序必须在考虑到这些复杂性的前提下进行设计和开发。 将服务调用视为同步功能调用会产生脆弱的应用程序。

编写服务以及使用服务的应用程序时,涉及的大多数复杂因素应该被提升到在应用程序要求和组织策略允许的前提下尽可能更广泛地共享的消息处理基础结构。 此基础结构将成为未来几年内整个计算机行业中创新和开发的关键方面。

 

 

 

版权所有:UML软件工程组织