Exploring the Observer Design Pattern微软技术文章翻译

本文翻译自微软技术文档 Exploring the Observer Design Pattern
原作者是Doug Purdy(微软)和 Jeffrey Richter(咨询和培训公司Wintellect)
译者:|Ringleader|@CSDN
转载文章请注明出处:🔗https://blog.csdn.net/weixin_44013533/article/details/134534367

简介

在一个给定的开发项目过程中,使用设计模式的概念来解决与应用设计和架构相关的一些问题并不罕见。然而,设计模式的定义通常很难准确地传达。因此,有必要对这一概念的起源和历史进行简要地考察。

软件设计模式的起源归功于克里斯托弗·亚历山大(Christopher Alexander)的工作。作为建筑师,亚历山大注意到在特定背景下存在着共同的问题和相关的解决方案。设计模式,正如亚历山大所称的问题/解决方案/背景三元组,使建筑师能够在建筑设计过程中以一种统一的方式迅速解决问题。25年前首次出版的《模式语言:城镇、建筑、建筑》(A Pattern Language: Towns, Buildings, Construction亚历山大等人,牛津大学出版社,1977年)介绍了250多个建筑设计模式,并为将这一概念引入软件开发领域奠定了基础。

1995年,软件行业首次广泛引入了设计模式,因其与应用程序的构建直接相关。四位作者Gamma、Helm、Johnson和Vlissides(合称四人组,或GoF)在他们的著作《设计模式:可复用面向对象软件的元素》(Addison-Wesley Pub Co,1995年)中将亚历山大的设计模式与蓬勃发展的面向对象软件行为相结合。基于他们的集体经验和对现有对象框架的研究,GoF提供了23种设计模式,这些模式考察了在设计和架构应用程序时遇到的常见问题和解决方案。在该书出版之后,设计模式的概念已经发展到包括软件领域中遇到的许多问题和解决方案。事实上,设计模式的流行催生了反模式的概念,这种解决方案通常会恶化而不是解决眼前的问题。

为什么使用设计模式?

虽然设计模式并非万能之术(如果这样的东西存在的话),但对于积极参与任何开发项目的开发人员或架构师来说,设计模式是一个非常强大的工具。设计模式确保通过众所周知和公认的解决方案解决常见问题。模式的基本优势在于,大多数问题可能已经被其他个人或开发团队遇到并解决了。因此,模式提供了在开发人员和组织之间共享可行解决方案的机制。无论这些模式的起源在哪里,它们都利用了这种集体知识和经验。这确保更快地开发正确的代码,并减少设计或实现中发生错误的机会。此外,设计模式为工程团队的成员之间提供了共同的语义。正如参与过大型开发项目的任何人都知道的那样,拥有一套共同的设计术语和原则对项目的成功完成至关重要。最重要的是,如果明智地使用设计模式,它可以释放出你的时间。

.NET框架模式

设计模式不是特定于某种语言或开发平台的(尽管GoF将他们的例子限制在C++和Smalltalk中);Microsoft .NET框架的出现为检查设计模式提供了新的机会和背景。在Framework Class Library(FCL)的开发过程中,Microsoft应用了1994年由GoF首次引入的许多相同的模式。由于.NET框架中公开的功能广泛,因此还开发并引入了全新的模式。

在本系列的过程中,我们将详细研究FCL中存在的几种设计模式。将考虑每种模式的一般结构和优点,然后考察FCL中的具体实现。尽管我们将要调查的大多数模式都源自GoF,但.NET框架提供了许多创新功能,目前还没有或几乎没有相关的设计指导。与这些新功能相关的设计模式也将被调查。我们对设计模式的研究始于观察者模式。

观察者模式

面向对象开发的首要原则之一是在给定的应用程序中正确分配责任。系统中的每个对象都应该关注问题域中的一个离散抽象,而不是更多。简而言之,一个对象应该只做一件事,并把它做好。这种方法确保了对象之间存在清晰的边界,从而实现更大程度的重用和系统可维护性。

责任划分特别重要的一个领域就是用户界面与底层业务逻辑之间的交互。在应用程序开发过程中,用户界面要求可能会迅速变化,而不会对应用程序的其余部分产生影响。此外,业务需求可能会不考虑用户界面而发生变化。在许多情况下,两组要求都会发生变化,这是有着丰富开发经验的人都知道的。在没有对用户界面和应用程序的其余部分进行分离的情况下,修改其中任一部分可能会对整体产生不利影响。

在用户界面和业务逻辑之间提供一个明确边界的需要是跨应用程序共同的问题。因此,自 GUI 诞生以来,许多面向对象的框架都支持将用户界面与应用程序的其余部分完全分离。这并不奇怪(或许有点奇怪),大多数采用了类似的设计模式来提供这种功能。这种通常称为“观察者”(Observer)的模式有助于在系统中明确区分各种对象。此外,在框架或应用程序的非用户界面相关部分中使用这种解决方案也是常见的。与大多数其他模式一样,观察者模式的有用性远远超出了最初的目的。

逻辑模型

虽然观察者模式存在许多变体,但该模式的基本前提涉及两个参与者,观察者和主题。在用户界面的上下文中,观察者是负责向用户显示数据的对象。另一方面,主题代表了从问题域建模的业务抽象。如图1所示,在观察者和主题之间存在逻辑关联。当主题对象发生变化(例如修改实例变量)时,观察者观察到这种变化并相应地更新其显示。

在这里插入图片描述

图1. 观察者和主题关系
  

例如,假设我们有一个简单的应用程序,可以在一天内跟踪股票价格。在这个应用程序中,我们有一个Stock类,该类模拟在纳斯达克交易的各种股票。该类包含一个实例变量,代表当前股票的卖出价(Ask Price,是银行卖出给投资人的价格,也即投资人要买进时参考报价),这个价格会在一天中上下浮动。为了将这些信息显示给用户,应用程序使用一个StockDisplay类,该类写入stdout(标准输出)。在这个应用程序中,Stock类的一个实例充当主题,StockDisplay类的一个实例充当观察者。随着交易日内卖出价的变化,Stock 实例的当前卖出价也会相应变化(变化的方式并不重要)。由于 StockDisplay 实例持续观察 Stock 实例,因此这些状态更改(卖出价的修改)会在发生时显示给用户。

使用这个观察过程确保了Stock和StockDisplay类之间存在边界。假设明天应用程序的要求发生变化,需要使用基于表单的用户界面。启用这一新功能只需简单地构建一个新类 StockForm 作为观察者。Stock类不需要任何修改。事实上,它甚至不知道进行了这样的更改。同样,如果需求变更为让Stock类从另一个来源(可能是Web服务而不是数据库)获取股票交易价格,那么StockDisplay类将不需要修改。它仍然继续观察Stock,毫不知情地接受任何更改。

物理模型

与大多数解决方案一样,细节是关键。观察者模式也不例外。尽管逻辑模型说明观察者观察主题,但在实现此模式时,这实际上是一个错误的说法。更准确地说,观察者在主题上注册( the observer registers with the subject),表示其对观察的兴趣。当状态发生变化时,主题通知观察者发生变化。当观察者不再希望观察主题时,观察者从主题中注销。这些步骤分别称为观察者注册、通知和注销。

大多数框架通过回调实现注册和通知。图2、图3和图4中显示的UML序列图对这些对象进行了建模,它们的方法调用一般就是用的这种方式。对于那些不熟悉序列图的人,最上面的矩形框代表对象,而箭头代表方法调用。

在这里插入图片描述

图2. 观察者注册
  

图2描绘了注册序列。观察者调用主题的Register方法,并将自己作为参数传递。一旦主题接收到此引用,它必须存储它以便在将来的某个时候通知观察者发生状态变化。与直接将观察者引用存储在实例变量中不同,大多数观察者实现将这个责任委托给一个单独的对象,通常是一个容器。 使用容器存储观察者实例带来重要的好处,我们稍后将对此进行讨论。序列图的下一个动作就是通过调用容器的Add方法来存储观察者实例的引用。【原句“The next action in the sequence is the storage of the observer reference denoted by the invocation of the Add method on the container.”对比能感受到中英文表达的差异】

在这里插入图片描述

图3. 观察者通知
  

图3强调了通知序列。当状态发生变化(AskPriceChanged)时,主题通过调用GetObservers方法检索容器中的所有观察者。然后,主题枚举检索到的观察者,调用Notify方法,通知观察者状态发生了变化。

在这里插入图片描述

图4. 观察者注销
  

图4显示了注销序列。当观察者不再需要观察主题时,观察者调用UnRegister方法,将自身作为参数传递。然后,主题在容器上调用Remove方法,结束观察期。

回到我们的股票应用程序,让我们剖析下注册和通知过程的影响。在应用程序启动期间,StockDisplay类的一个实例在Stock实例中注册,将自身作为参数传递给Register方法。Stock实例持有对StockDisplay实例的引用(在容器中)。当股票价格属性更改时,Stock实例会调用Notify方法,将变更通知StockDisplay。当应用程序关闭时,StockDisplay实例从Stock实例中注销,调用UnRegister方法,终止两个实例之间的关系。

值得注意下使用容器而不是实例变量来存储观察者的引用的好处。假设我们的需求除了当前的用户界面StockDisplay之外,还需要一个交易日期间的实时股票卖价图。为此,我们创建了一个名为StockGraph的新类,它在y轴上绘制卖价,在x轴上绘制一天中的时间。当应用程序启动时,它将StockDisplay和StockGraph类的实例都注册到Stock实例中。由于主题将观察者存储在容器而不是实例变量中,因此这不会造成问题。当价格发生变化时,Stock实例会将状态变化通知其容器中的两个观察者实例。可以看出,使用容器可以让每个主题灵活支持多个观察者。这允许主题通知潜在的无限数量的观察者其状态变更,而不仅仅是一个。

虽然这不是一个要求,但许多框架提供了一组用于观察者和主题实现的接口。如下C#代码中所示,IObserver接口公开一个名为Notify的公共方法。这个接口由所有打算充当观察者的类实现。IObservable接口由所有打算充当主题的类实现,公开两个方法,Register和UnRegister。这些接口通常采用抽象虚拟类或真实接口的形式,如果实现语言支持这样的构造。使用这些接口有助于减少观察者和主题之间的耦合。与观察者和主题类之间的紧密耦合关系不同,IObserver和IObservable接口允许进行独立实现。通过审查这些接口,你会注意到所有方法都被类型化为在接口类型上操作,而不是在具体类上。这种方法将接口编程模型的好处扩展到观察者模式。

IObserver and IObservable interfaces (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为不同的观察者或添加额外的观察者实例)。

除了这些接口外,一个框架通常还会为主题提供一个公共基类。扩展此基类减少了支持观察者模式所需的工作量。基类实现了IObservable接口,提供支持存储和通知观察者实例所需的基础设施。以下是名为ObservableImpl的这样一个基类的C#代码示例。这个类将观察者的存储委派给Register和UnRegister方法中的Hashtable实例,尽管可能任何容器都足够了(为了方便起见,我们的示例使用Hashtable作为容器,只需要一个方法调用就可以注销特定的观察器实例)。还请注意增加的NotifyObservers方法。此方法用于通知Hashtable中存储的观察者。当调用此方法时,对容器进行枚举,调用观察者实例的Notify方法。

ObservableImpl Class (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类进行任何更改。这种方法真正简化了观察者模式的实现,允许多个主题重用相同的功能,同时在所涉及的类之间保持松散耦合的关系。

在下面的C#示例中,观察者模式的例子突显了在我们的股票应用程序上下文中使用IObservable和IObserver接口以及ObservableBase类。除了Stock和StockDisplay类之外,这个例子还使用MainClass来关联观察者和主题实例,并修改Stock实例的AskPrice属性。该属性负责调用基类的NotifyObserver方法,该方法依次通知实例相关的状态变更。

Observer Example (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 Framework中的观察者模式

基于我们对观察者模式的理解,现在让我们将注意力转向该模式在.NET Framework中的使用。对于那些熟悉FCL中的类型的人来说,你会注意到在Framework中没有IObserver、IObservable或ObservableImpl类型。它们缺失的主要原因是CLR在某种程度上使它们过时。尽管您依然可以在.NET应用程序中使用这些构造,但是引入委托和事件提供了一种新而强大的实现观察者模式的方法,而无需开发专门支持此模式的特定类型。实际上,由于委托和事件是CLR的一等成员,这种模式的基础已经融入.NET Framework的核心。因此,FCL在其结构中广泛使用观察者模式。

有关委托和事件内部工作的内容已经有很多文章,因此这里无需进行类似的描述。可以简单地说,委托是面向对象(且类型安全)的函数指针的等价物。委托实例持有对实例或类方法的引用,允许匿名调用所绑定的方法。事件是在类上声明的特殊构造,有助于在运行时向感兴趣的对象公开状态更改。事件代表我们之前用于实现观察者模式中的注册、注销和通知方法的正式抽象(被CLR和各种编译器所支持)。委托在运行时向特定事件注册。引发事件时,将调用所有注册的委托,以便它们接收事件通知。 有关委托和事件更深入的介绍,请参阅An Introduction to Delegates

在研究观察者模式中的委托和事件之前,值得注意的是,CLR 支持的各种语言都可以按照语言设计者的意愿自由地公开委托和事件机制。因此,在各种语言中对这些功能的全面研究是不可能的。在下面的讨论中,我们将重点放在C#对这些功能的实现上。如果您使用的是C#之外的语言,请参阅相应语言的文档,了解有关委托和事件在该语言中的支持情况的更多信息。

用观察者模式的术语来说,声明事件的类就是主题 。与我们先前使用的IObservable接口和ObservableImpl类不同,主题类无需实现给定的接口或扩展基类。主题只需要公开一个事件即可,无需更多操作。创建观察者略微复杂一些,但更加灵活(我们稍后将讨论这一点)。观察者不是实现IObserver接口并将自身注册到主题,而是必须创建特定委托实例,并使用此委托注册到主题的事件上。观察者必须使用由事件声明时指定的类型的委托实例,否则注册将失败。在创建此委托实例期间,观察者将要由主题通知的方法(实例或静态)名称传递给委托。一旦委托绑定到方法,它就可以注册到主题的事件。同样,此委托也可以从事件中注销。主题通过调用事件来向观察者提供通知。

如果您对委托和事件不熟悉,实现观察者模式可能会显得很麻烦,特别是与我们先前使用的IObserver和IObservable接口相比。然而,实际上它比听起来简单得多,而且更容易实现。以下是C代码示例,其突显了在我们的示例应用程序中支持委托和事件所需的类修改。请注意,Stock或StockDisplay类没有使用任何基类或接口来支持该模式。

Observer using delegates and events (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 基于事件和委托广泛使用了观察者模式。FCL 的设计者充分认识到这种模式的潜在力量,并将其应用于框架中的用户界面和非用户界面特定功能。然而,这种使用略有变化,框架团队将其称为事件模式。一般来说,这种模式被视为事件通知过程中涉及的委托、事件和相关方法的正式命名约定。Microsoft 建议所有使用事件和委托的应用程序和框架采用这种模式,尽管在CLR或标准编译器中并没有强制执行(当心模式警察!)。

这一系列约定中的第一个,也可能是最重要的,是主题公开的事件的名称。对于它所代表的状态变化,这个名称应该是不言自明的。请记住,这种约定与所有其他类似的约定一样,是主观的。其目的是为那些使用你的事件的人提供清晰的信息。事件模式的其余部分利用事件的正确命名,因此这一步对于该模式至关重要。

回到我们可靠的例子,让我们来看看这个约定对Stock类的影响。推导事件名称的一种合适方法是使用在主题类中修改的字段的名称作为根。由于在Stock类中被修改的字段的名称是_askPrice,一个合理的事件名称将是AskPriceChanged。很显然,这个事件的名称要比像在StockClass中的StateChangedInStockClass这样的名称更具描述性。因此,AskPriceChanged事件名称符合第一个约定。

事件模式中的第二个约定是委托及其签名的正确命名。委托的名称应该由事件名称(通过第一个约定选择)和附加的Handler一词组成。该模式要求委托指定两个参数,第一个提供对事件发送者的引用,第二个向观察者提供上下文信息。第一个参数的名称只是sender。该参数的类型必须为System.Object。这是因为委托可以绑定到系统中任何类的任何方法。第二个参数的名称比第一个更简单,是e。该参数的类型为System.EventArgs或某个派生类(稍后详细说明)。尽管委托的返回类型取决于您的实现需求,但大多数实现此模式的委托根本不返回任何值。

委托的第二个参数e需要稍微注意下。该参数使主题对象能够向观察者传递任意上下文信息。如果不需要此类信息,System.EventArgs的实例就足够了,因为该类的实例表示缺少上下文数据。否则,应构建一个派生自System.EventArgs的类,其中包含适当的实现以提供这些数据。这个类的命名应该以事件名称为基础,然后附加上"EventArgs"一词。

参考我们的Stock类,这个约定要求处理AskPriceChanged事件的委托应命名为AskPriceChangedHandler。此委托的第二个参数的名称应该命名为AskPriceChangedEventArgs。由于我们需要将新的股票卖出价传递给观察者,我们需要继承System.EventArgs类,将这个类命名为AskPriceChangedEventArgs,并提供支持传递此数据的实现。

事件模式中的最后一项是负责触发事件的主题类上的方法的名称和可访问性。该方法的名称应由事件名称和一个前缀On组成。此方法的可访问性应设置为protected。这一约定仅适用于非密封类,因为它充当了一个众所周知的调用点,供派生类调用在基类注册的观察者。

将这个最后的约定应用于Stock类,就完成了事件模式。由于Stock类没有密封,我们必须添加一个方法来触发事件。根据这个模式,该方法的名称是OnAskPriceChanged。下面的C#代码示例展示了应用于Stock类的事件模式的完整视图。请注意我们对System.EventArgs类的专门化的使用。

Event Pattern Example (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

结论

根据对观察者模式的探讨,可以明显看出,这种模式为确保应用程序中对象之间的清晰边界提供了理想的机制,无论它们的功能是什么(UI或其他)。虽然通过回调(使用IObserver和IObservable接口)相对简单,但CLR中委托和事件的概念处理了大部分的“繁重工作”,同时降低了主题和观察者之间的耦合水平。正确使用这种模式可以大大确保应用程序能够不断发展。随着用户界面和业务需求的不断变化,观察者模式将确保您的工作不再那么艰难。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值