本文章是由機器翻譯。

領先技術

面向方面的程式設計、偵聽和 Unity 2.0

Dino Esposito

毫無疑問,物件導向是一種主流程式設計模式,當涉及到將某個系統分割為元件並通過元件來描述過程時,這種模式佔有優勢。 當處理某元件的業務特定關注點時,物件導向 (OO) 模式同樣佔有優勢。 但是,當涉及到處理橫切關注點時,OO 模式不再有效。 一般來說,橫切關注點是一個在系統中影響多個元件的關注點。

為了最大限度地重用複雜的業務邏輯代碼,您通常傾向于圍繞系統的核心和主要業務功能設計類的層次結構。 但其他橫切類層次結構的非業務特定關注點該如何實現? 緩存、安全和日誌記錄等功能在什麼位置適合? 很可能就是在每個受影響的物件中重複使用這些功能。

橫切關注點是必須在一個不同的邏輯級別(超出應用程式類範圍的級別)處理的系統的一個方面,而不是給定元件或系列元件的特定職責。 出於此原因,多年前就定義了一個不同的程式設計模式:面向方面的程式設計 (AOP)。 順便說一下,AOP 這一概念于 20 世紀 90 年代在 Xerox PARC 實驗室中產生。 該團隊還開發出第一種 AOP 語言(仍是最受歡迎的):AspectJ。

儘管幾乎所有人都認同 AOP 的好處,但它仍未廣泛實現。 在我看來,這種應用範圍有限的主要原因基本上是缺乏合適的工具。 我深信,Microsoft .NET Framework 本機支援 AOP(即使只是部分支援)的那一天將成為 AOP 的歷史轉捩點。 現在,您只能使用 ad hoc 框架在 .NET 中實現 AOP。

.NET 中 AOP 的最強大工具是 PostSharp,您可在 sharpcrafters.com 中找到。 PostSharp 提供一個完整的 AOP 框架,您可在該框架中體驗 AOP 理論的所有關鍵功能。 但應注意,許多依賴關係注入 (DI) 框架都包括一些 AOP 功能。

例如,您會在 Spring.NET、Castle Windsor 當然還有 Microsoft Unity 中發現 AOP 功能。 對於相對簡單的方案(例如,在應用層跟蹤、緩存和修飾元件),DI 框架的功能通常能成功應用。 但對於域物件和 UI 物件,很難使用 DI 框架獲得成功。 無疑,橫切關注點會被視為外部依賴關係,而 DI 技術也必定允許您在類中注入外部依賴關係。

關鍵在於,DI 很可能將要求進行 ad hoc 前期設計或稍做重構。 換句話說,如果您已在使用 DI 框架,則很容易就能導入一些 AOP 功能。 反之,如果您的系統未使用 DI,則導入 DI 框架可能需要相當多的工作。 這在大型專案中或在更新舊系統的過程中並不總是可能實現的。 通過改用典型的 AOP 方法,可在一個稱為“方面”的新元件中包裝所有橫切關注點。 在本文中,首先我將向您快速概述一下麵向方面的模式,然後介紹您在 Unity 2.0 中發現的 AOP 相關功能。

AOP 快速指南

物件導向的程式設計 (OOP) 專案由多個原始檔案組成,每個原始檔案實現一個或多個類。 該專案還包括表示橫切關注點(如日誌記錄或緩存)的類。 所有類均由編譯器處理並生成可執行代碼。 在 AOP 中,一個方面表示一個可重用的元件,它將多個類所需的行為封裝在專案中。 實際處理方面的方式取決於您所考慮的 AOP 技術。 通常情況下,我們可以說各個方面並不簡單直接地由編譯器進行處理。 若要修改可執行代碼以將方面考慮在內,需要一種額外的特定于技術的工具。 讓我們大致看一下使用 AspectJ(第一個創建的 AOP 工具,即 Java AOP 編譯器)會發生什麼情況。

借助 AspectJ,您可使用 Java 程式設計語言來編寫您的類,並使用 AspectJ 語言來編寫方面。 AspectJ 支援自訂語法,您可通過自訂語法指示方面的預期行為。 例如,日誌記錄方面可能指定它將在調用特定方法之前和之後記錄。 各個方面以某種方式合併到常規原始程式碼中並產生原始程式碼的中間版本,然後將該中間版本編譯成可執行格式。 在 AspectJ 術語中,預處理方面並將方面與原始程式碼合併的元件稱為 weaver。 該元件產生一個編譯器可呈現給可執行檔的輸出。

總之,一個方面描述一段可重用的代碼,您希望將可重用代碼注入現有類中,而不接觸這些類的原始程式碼。 在其他 AOP 框架(如 .NET PostSharp 框架)中,您將找不到 weaver 工具。 但是,方面的內容始終由框架進行處理並生成某種形式的代碼注入。

請注意,在這方面上,代碼注入不同于依賴關係 注入。 代碼注入是指,AOP 框架能夠將對方面中特定點處的公共終結點的調用插入到使用給定方面修飾的類主體中。 舉例來說,PostSharp 框架讓您能夠將方面編寫為 .NET 屬性,然後將這些屬性附加到類中的方法上。 PostSharp 屬性由 PostSharp 編譯器(我們甚至可以稱之為 weaver)在生成後步驟中進行處理。 實際效果是,您的代碼得到增強,從而在這些屬性中包括一些代碼。 但注入點將得到自動解析,您作為一名開發人員只需編寫一個獨立方面元件並將其附加到公共類方法即可。 代碼易於編寫,甚至更易於維護。

為了完成此次有關 AOP 的快速概述,我將介紹一些特定術語並解釋它們各自的含義。 聯接點指示您要在目標類的原始程式碼中注入方面代碼的點。 pointcut 表示聯接點集合。 建議指的是要在目標類中注入的代碼。 可在聯接點的前後和四周注入代碼。 一個建議與一個 pointcut 關聯。 這些術語來自 AOP 的原始定義,在您使用的特定 AOP 框架中可能不會反映在字面上。 建議您嘗試選取這些術語隱含的概念(AOP 的核心概念),然後使用這種知識更好地瞭解特定框架的詳細資訊。

Unity 2.0 快速指南

Unity 是作為 Microsoft Enterprise Library 專案的一部分提供的應用區塊,它也可單獨下載。 Microsoft Enterprise Library 是應用區塊的集合,該集合處理大量描述 .NET 應用程式開發特徵的橫切關注點,如日誌記錄、緩存、加密、異常處理等。 Enterprise Library 的最新版本是 5.0,于 2010 年 4 月份發佈,並附帶對 Visual Studio 2010 的完全支援(在 msdn.microsoft.com/library/ff632023 上的“模式和實踐開發人員中心”處,可瞭解該版本的詳細資訊)。

Unity 是 Enterprise Library 應用區塊之一。 Unity 同樣適用于 Silverlight,它實質上是為攔截機制提供額外支援的 DI 容器,通過攔截機制可使您的類更加面向方面。

Unity 2.0 中的攔截功能

Unity 中攔截的核心理念是讓開發人員能夠自訂調用鏈,方便對物件調用方法。 也就是說,Unity 攔截機制通過在方法的常規執行前後或四周額外添加一些代碼,捕獲對已配置物件進行的調用並自訂目標物件的行為。 攔截實際上是在運行時向物件中添加新行為的一種極其靈活的方法,無需接觸到物件的原始程式碼,也不會影響相同繼承路徑中的類的行為。 Unity 攔截是實現 Decorator 模式的一種方式,該模式是一種常用設計模式,設計為在運行時擴展正在使用的物件的功能。 Decorator 是一個容器物件,它接收目標物件的實例(和維護對實例的引用),並向外界擴充其功能。

Unity 2.0 中的攔截機制同時支援實例攔截和類型攔截。 此外,不管產生實體物件的方式如何,無論物件是通過 Unity 容器創建的還是一個已知實例,攔截都照常工作。 在後一種情況下,您只需使用一個不同的完全獨立的 API 即可。 但是,如果您這麼做,則將丟失設定檔支援。 圖 1 演示 Unity 中攔截功能的體系結構,並詳細說明該功能在未通過容器解析的特定物件實例上的工作方式。 (此圖只是對 MSDN 文檔中的某幅圖稍做了一些修改。)


圖 1 Unity 2.0 中物件攔截的工作方式

攔截子系統由三個關鍵元素組成:偵聽器(或代理);行為管道;以及行為或方面。 這些子系統的兩個極端分別為用戶端應用程式和目標物件(即,被分配了未在其原始程式碼中進行硬編碼的其他行為的物件)。 在將用戶端應用程式配置為在給定實例上使用 Unity 的攔截 API 後,所有方法調用都將通過一個代理物件(偵聽器)。 此代理物件查看已註冊行為的清單,並通過內部管道調用這些行為。 每個配置的行為都有機會在物件方法的常規調用之前或之後運行。 該代理將輸入資料注入到管道中,然後在資料經目標物件最初生成接著由行為進一步修改後,該代理接收任何返回值。

配置攔截

在 Unity 2.0 中建議使用攔截的方法不同于早期版本,儘管在早期版本中使用的方法完全支援向後相容性。 在 Unity 2.0 中,攔截只是您添加到容器中的一個新擴展,用來描述物件的實際解析方式。 下麵是您希望通過 Fluent 代碼配置攔截時所需的代碼:

var container = new UnityContainer();
container.AddNewExtension<Interception>();

該容器需要查找有關要攔截的類型和要添加的行為的資訊。 可使用 Fluent 代碼或通過配置添加此資訊。 我發現配置特別靈活,因為您無需接觸應用程式也無需執行任何新的編譯步驟,即可修改一些內容。 讓我們採用基於配置的方法。

首先,在設定檔中添加以下內容:

<sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.
  Configuration.InterceptionConfigurationExtension, 
  Microsoft.Practices.Unity.Interception.Configuration"/>

此腳本的目的是使用特定于攔截子系統的新元素和別名來擴展配置架構。 另外,添加以下內容:

<container> 
  <extension type="Interception" /> 
  <register type="IBankAccount" mapTo="BankAccount"> 
    <interceptor type="InterfaceInterceptor" /> 
    <interceptionBehavior type="TraceBehavior" /> 
  </register> 
</container>

若要使用 Fluent 代碼實現相同的任務,您需要對容器物件調用 AddNewExtension<T>和 RegisterType<T>。

讓我們進一步看一下配置腳本。 <extension>元素將攔截添加到容器中。 請注意,腳本中使用的“Interception”是在節擴展中定義的別名之一。 介面類別型 IBankAccount 映射到具體類型 BankAccount(這是 DI 容器的典型作業),並與特定類型的偵聽器相關聯。 Unity 提供兩種主要類型的偵聽器:實例偵聽器和類型偵聽器。 下個月,我將深入探討偵聽器。 現在,一句話說明,實例偵聽器創建一個代理來篩選針對已截獲實例傳入的調用。 相反,類型偵聽器只是類比已截獲物件的類型,並在派生類型的實例上工作。 (有關偵聽器的詳細資訊,請參閱 msdn.microsoft.com/library/ff660861(PandP.20)。)

介面偵聽器是僅限於充當物件上一個介面的代理的實例偵聽器。 介面偵聽器使用動態代碼生成來創建代理類。 配置中的攔截行為元素指示您要圍繞已截獲物件實例運行的外部代碼。 必須通過聲明的方式配置 TraceBehavior 類,以便容器可以解析該類及其任何依賴關係。 使用 <register>元素告知容器 TraceBehavior 類及其所需的構造函數,如下所示:

<register type="TraceBehavior"> 
   <constructor> 
     <param name="source" dependencyName="interception" /> 
   </constructor> 
</register>

圖 2 顯示 TraceBehavior 類中的一段摘錄。

圖 2 Unity 行為示例

class TraceBehavior : IInterceptionBehavior, IDisposable
{
  private TraceSource source;
  public TraceBehavior(TraceSource source)
  {
    if (source == null) 
      throw new ArgumentNullException("source");
    this.source = source;
  }
   
  public IEnumerable<Type> GetRequiredInterfaces()
  {
    return Type.EmptyTypes;
  }
  public IMethodReturn Invoke(IMethodInvocation input, 
    GetNextInterceptionBehaviorDelegate getNext)
  {
     // BEFORE the target method execution 
     this.source.TraceInformation("Invoking {0}",
       input.MethodBase.ToString());
     // Yield to the next module in the pipeline
     var methodReturn = getNext().Invoke(input, getNext);
     // AFTER the target method execution 
     if (methodReturn.Exception == null)
     {
       this.source.TraceInformation("Successfully finished {0}",
         input.MethodBase.ToString());
     }
     else
     {
       this.source.TraceInformation(
         "Finished {0} with exception {1}: {2}",
         input.MethodBase.ToString(),
         methodReturn.Exception.GetType().Name,
         methodReturn.Exception.Message);
     }
     this.source.Flush();
     return methodReturn;
   }
   public bool WillExecute
   {
     get { return true; }
   }
   public void Dispose()
   {
     this.source.Close();
   }
 }

行為類實現 IinterceptionBehavior,它基本上由 Invoke 方法組成。Invoke 方法包含您要用於受偵聽器控制的任何方法的整個邏輯。如果您想要在調用目標方法之前執行一些操作,則在該方法開頭執行操作。當您想要運行到目標物件(或者更準確的說是運行到管道中註冊的下一個行為)時,需調用框架提供的 getNext 委派。最後,您可使用任何所需的代碼對目標物件進行後處理。Invoke 方法需要返回對管道中下一個元素的引用;如果返回 Null,則鏈中斷,後續的行為將永遠不會被調用。

配置靈活性

攔截(更籠統的說是 AOP)滿足了許多有用的方案的要求。例如,利用攔截,您可向各個物件中添加責任,而無需修改整個類,並且保持解決方案相對於使用 Decorator 來說更加靈活。

本文只涉及了應用於 .NET 的 AOP 的一些皮毛。在接下來的幾個月裡,我將撰寫有關 Unity 和 AOP 中攔截的更多內容。

Dino Esposito  是 Microsoft Press (2010) 出版的《Programming Microsoft ASP.NET MVC》一書的作者,並且是《Microsoft .NET:Architecting Applications for the Enterprise》(Microsoft Press,2008 年)的合著者。Esposito 定居於義大利,經常在世界各地的業內活動中發表演講。您可訪問他的博客,網址為 weblogs.asp.net/despos

衷心感謝以下技術專家對本文的審閱:Chris Tavares