微軟企業庫5.0 學習之路——擴展學習篇、庫中的依賴關係注入(重構 Microsoft Enterprise Library)[轉]

這篇文章是我在patterns & practices看到的一篇有關EntLib5.0的文章,主要介紹了EntLib5.0的此次的架構變化由來,以爲很不錯,你們能夠看一下!ios

 

在過去幾年中,依賴關係注入 (DI) 模式在 .NET 開發人員社區一直受到關注。長時間以來,優秀的博客做者們討論着 DI 的優勢。MSDN 雜誌 針對這一主題發表了多篇文章。.NET 4.0 將發佈某種相似 DI 的功能,並計劃之後將其發展爲完整的 DI 系統。數據庫

閱讀有關 DI 的博客文章時,我注意到,這一主題有一個很小卻很重要的傾向。做者們談論的是如何在整個應用程序環境中使用 DI。但如何編寫使用 DI 的庫或框架呢?關注重點的變化,對模式的使用有何影響?這是幾個月前咱們研究 Enterprise Library 5.0 的體系結構時首先遇到的問題。express

背景

Microsoft Enterprise Library (Entlib) 是 Microsoft 模式與實施方案組開發的著名版本。迄今爲止,其下載次數已超過兩百萬。能夠想到的單位 — 從金融機構、政府機關到餐廳和醫療設備製造商 — 都在使用它。顧名思義,Entlib 是一種庫,可幫助開發人員處理許多企業開發人員都會面臨的問題。若是您不熟悉 Entlib,請訪問咱們的網站 p&p 開發中心,以瞭解更多信息。緩存

Entlib 在很大程度上由配置驅動。它的大部分代碼專用於讀取配置,而後基於配置組合對象圖。Entlib 對象可能很是複雜。大多數塊都包含大量可選功能。此外,還有許多用於支持檢測等功能的底層基礎結構,它們也須要進行關聯。咱們不但願用戶僅僅爲了使用 Entlib 而去手動建立檢測提供程序、讀取配置,等等,因此將對象的建立封裝在了工廠對象和靜態外層以後。性能優化

Entlib 版本 2 到版本 4 的核心是一個名爲「ObjectBuilder」的小型框架。ObjectBuilder 的做者將 ObjectBuilder 描述爲「一種用於構建依賴關係注入容器的框架」。Enterprise Library 只是使用 ObjectBuilder 的 p&p 項目之一;其餘使用 ObjectBuilder 的 p&p 項目包括 Composite UI Application Block、Smart Client Software Factory 和 Web Client Software Factory。Entlib 特別注重說明的「框架」部分,將一個很大的自定義功能集構建至 ObjectBuilder。讀取 Entlib 配置和組合對象圖時,須要使用這些自定義功能。在不少狀況下,也須要用它們來改進現有 ObjectBuilder 實現的性能。架構

缺點在於,須要很多時間才能對 ObjectBuilder 自己(設計極爲抽象,再加上徹底沒有文檔,ObjectBuilder 的複雜性絕非虛言)和 Entlib 自定義功能都有所瞭解。所以,若是要編寫與 Entlib 的對象建立策略有關的自定義塊,一開始就須要進行大量學習,經常使人感受困難重重。框架

此外,在 Entlib 4.0 中,咱們發佈了 Unity 依賴關係注入容器,這進一步增長了複雜性。DI 具備不少優勢,咱們但願確保爲沒法從衆多優秀開放源代碼容器中選用一種(不管什麼緣由)的客戶提供一個很好的選擇 — Microsoft 的 DI。固然,咱們也但願在使用 Unity 時輕鬆實現 Entlib 對象的運行。在 Entlib 4.0 中,Unity 集成與現有 ObjectBuilder 基礎結構一道,成爲了並行對象建立系統。如今,塊編寫者不只須要瞭解 ObjectBuilder 和 Entlib 擴展,還須要瞭解 Unity 內部機制,以及其中的部分 Entlib 擴展。這不是朝正確的方向前進。ide

致力於簡化

2009 年 4 月,咱們開始 Entlib 5.0 的開發。這一版本的主要目的是「以簡化取勝」。這不只包括爲最終用戶(調用 Entlib 的開發人員)進行簡化,也包括對 Entlib 代碼自己進行簡化。經過這些改進,咱們能夠更方便地保持 Entlib 的進一步發展,客戶也能夠更方便地對它進行了解、自定義和擴展。函數

咱們知道,有些重要方面須要改進,其中之一是對象建立管道。保留兩個並行但不一樣的代碼集實現同一功能會後患無窮。必須改變這種狀況。性能

咱們制定瞭如下重構目標:

  • 現有客戶端代碼沒必要僅因體系結構更改而更改。可要求從新編譯,但不可要求更改源代碼(固然,客戶端 API 能夠因其餘 緣由進行更改)。可處理內部 API 或可擴展 API。
  • 刪除冗餘對象建立管道。應只保留一種(而非兩種或更多)建立對象的方式。
  • 不使用 DI 的客戶不該受在內部使用 DI 的 Entlib 的影響。
  • 確實須要 DI 的客戶能夠選擇所需容器,而後從中獲取本身的對象和 Entlib 對象。

不管從單獨仍是組合的角度來說,這些目標都意味着要進行大量工做。從表面看,「一個對象建立管道」目標至關簡單。咱們決定徹底刪除基於 ObjectBuilder 的系統,在內部採用一個 DI 容器做爲對象建立引擎。可是,咱們須要考慮「不該更改現有客戶端代碼」。傳統 Entlib API 是一組靜態外層和工廠。例如,使用日誌記錄塊來記錄一條消息可採用以下方式:

1
Logger.Write( "My Message" );

實際上,Logger 外層使用 LogWriter 對象的實例執行實際工做。那麼,Logger 外層如何得到 LogWriter?LogWriter 是一個至關複雜的類,具備大量依賴關係,所以,若是採用新建的方式,配置是沒法正確關聯的。咱們認爲,在 API 中,Logger 和全部其餘靜態類須要一個全局容器實例。咱們能夠僅保留一個全局 Unity 容器,可是,咱們須要考慮「客戶選擇所需容器」。

咱們但願 Unity 和 Entlib 組合能實現一流的體驗。咱們也但願經過其餘容器也能實現這種一流體驗。儘管 DI 容器的常規功能都一致,但訪問這些功能的方式卻有很大差別。實際上,許多容器建立者都認爲他們的配置 API 是主要競爭優點。所以,咱們如何將 Entlib 配置映射到差別很大的容器 API 上?

傳統計算機科學解決方案

這是計算機科學領域公認的事實:計算機科學中的全部問題均可以經過添加一個間接層解決。這正是咱們解決容器獨立問題的方法。咱們把這個間接層稱爲容器配置程序。從本質上說,配置程序的做用是讀取 Entlib 的配置,並對容器進行配置以便匹配。

遺憾的是,讀取配置自己還不夠。Entlib 的配置文件格式很大程度上是以最終用戶爲中心的。用戶配置日誌記錄類別、異常策略和緩存後備存儲。但不說明要完成相應功能實際所需的對象、要向構造函數傳 遞的值以及要設置的屬性。另外一方面,DI 容器配置的內容則是「將此界面映射到此類型」、「調用此構造函數」和「設置此屬性」等。咱們須要另外一個間接層將塊的配置映射到實際所需對象來實現塊。另外一 種方法是,讓每個配置程序(每一個容器都須要一個配置程序)都知道每個塊的詳細信息。很明顯,這不可行;對塊代碼進行任何更改都將波及全部配置程序。如 果有人編寫自定義塊,會發生什麼狀況?

咱們最後開發了一組名爲「TypeRegistration」的對象。各配置節負責生成一個類型註冊模型 ,一系列 TypeRegistration 對象。TypeRegistration 的接口如圖 1 所示。

圖 1 TypeRegistration 類

public class TypeRegistration
    {
 
        public TypeRegistration(LambdaExpression expression);
        public TypeRegistration(LambdaExpression expression, Type serviceType);
 
        public Type ImplementationType { get; }
        public NewExpression NewExpressionBody { get; }
        public Type ServiceType { get; private set; }
        public string Name { get; set; }
 
        public static string DefaultName(Type serviceType);
        public static string DefaultName<TServiceType>();
 
        public LambdaExpression LambdaExpression { get; private set; }
 
         public bool IsDefault { get; set; }
 
         public TypeRegistrationLifetime Lifetime { get; set; }
 
         public IEnumerable<ParameterValue> ConstructorParameters { get; }
 
         public IEnumerable<InjectedProperty> InjectedProperties { get; }
    }

 

該類的內容不少,但基本結構很是簡單。該類描述單個類型所需的配置。ServiceType 是用戶從容器進行請求的接口,而 ImplementationType 則是實際實現該接口的類型。Name 是註冊服務時應使用的名稱。生存期可肯定單一實例(每次都返回同一實例)或瞬態(每次都建立新的實例)建立行爲。其餘在此就不一一列舉了。咱們選擇使用 lambda 表達式來建立 TypeRegistration 對象,由於這樣能夠很是方便地在單一緊湊的範圍內指定全部這些信息。如下是從數據訪問塊建立類型註冊的示例:

yield return new TypeRegistration<Database>(
       () => new SqlDatabase(
           ConnectionString,
           Container.Resolved<IDataInstrumentationProvider>(Name)))
       {
           Name = Name,
           Lifetime = TypeRegistrationLifetime.Transient
       };

此類型註冊表示「若是請求名爲 Name 的數據庫,則返回一個新的 SqlDatabase 對象,該對象由 ConnectionString 和 IDataInstrumentationProvider 構造」。此處使用 lambda 的好處在於,在編寫塊時,可像直接新建對象同樣構建這些表達式。編譯器將對錶達式進行類型檢查,這樣,咱們就不會在無心中調用不存在的構造函數了。若要設 置屬性,可在 lambda 表達式內使用 C# 對象初始值設定項語法。TypeRegistration 類負責處理檢查 lambda、提取構造函數簽名、參數、類型等等的詳細信息,以避免配置程序做者爲之操心。

咱們用過的一個實用的技巧是調用「Container.Resolved」。該方法實際上不執行任何操做,它的實現以下:

public static T Resolved<T>(string name)
{
     return default(T);
}

 

爲何要用它?請注意,此 lambda 表達式實際上從不執行。相反,咱們是在運行時經過運行表達式的結構提取註冊信息。此方法只是一個衆所周知的標記。若是將對 Container.Resolved 的調用做爲參數,咱們解釋爲「經過容器解析此參數」。咱們發現,用表達式樹執行高級工做時,此標記方法技術在不少狀況下頗有用。

最後,配置的容器的配置文件流程如圖 2 所示。


圖 2 容器配置

此處要說明一下咱們的一項設計決策,這很是重要。TypeRegistration 系統如今不是(之後也毫不會成爲)任何 DI 容器的通用、全面配置抽象概念。它是應 Enterprise Library 項目之需專門設計的。模式和實施方案組無心將它做爲基於代碼的指南。儘管基本概念(將配置提取到抽象模型中)廣泛適用,此處的特定實現僅適用於 Entlib。

從容器中獲取對象

這樣,咱們就配置了容器。這隻完成了一半工做。如何才能從容器中獲取對象?在這方面,容器接口各不相同,使人欣慰的是,這種不一樣沒有其配置接口那樣大。

很幸運,這時咱們沒必要創造新的抽象概念。受 2008 年夏 Jeremy Miller 發表的博客文章的啓發,Microsoft 的模式和實施方案組、MEF 團隊和許多不一樣的 DI 容器的做者們合做,定義了一個最低通用標準,以解決從容器中解析出對象的問題。該標準做爲 Common Service Locator 項目發佈在 Codeplex 和 MSDN 上。該接口正好知足咱們的須要;在 Enterprise Library 中,不管什麼時候須要從容器中獲取對象,均可經過該接口進行調用,並與所用的特定容器隔離開。固然,下一個問題是:容器在哪裏?

Enterprise Library 沒有任何類型的引導需求。使用靜態外層時,不須要在任何位置調用初始化函數。首次須要原始庫時,可經過提取配置來運行它。咱們必須複製此行爲,以便在調用時,庫已準備就緒可供使用。

咱們須要的是衆所周知的標準庫,以便獲取正確配置的容器。實際上,Common Service Locator 庫具備如下功能之一:ServiceLocator.Current 靜態屬性。因爲種種緣由,咱們決定不使用此屬性。主要緣由是,其餘庫,甚至應用程序自己均可使用 ServiceLocator.Current。咱們須要在首次訪問任何 Entlib 項目時,可以對容器進行設置;其餘都不重要,好比人們試圖弄明白爲什麼其認真構建的容器會消失,或爲什麼 Entlib 在首次調用能夠運行,但後來就不行了。第二個緣由與接口自己的一個缺陷有關。沒法查詢該屬性,於是不能肯定是否已對其進行了設置。這樣就很難肯定什麼時候設置 容器。

所以,咱們構建了本身的靜態屬性:EnterpriseLibraryContainer.Current。在用戶代碼中也能夠設置此屬性,但它是 Enterprise Library 的特定部分,所以,減少了與其餘庫或主應用程序發生衝突的可能性。首次調用靜態外層時,應檢查 EnterpriseLibraryContainer.Current。若是已設置,則可以使用其值。若是未設置,則應建立一個 UnityContainer 對象,用配置程序對其進行配置,並將其設置爲 Current 屬性的值。

這樣,如今就有了三種不一樣的方式,可訪問 Enterprise Library 的功能。若是使用傳統 API,一切都會正常運行。在底層,將建立和使用 Unity 容器。若是要在應用程序中使用不一樣的 DI 容器,不但願進程中有 Unity,但仍使用傳統 API,則可使用配置程序來配置您的容器,將其封裝在 IServiceLocator 中,並附於 EnterpriseLibraryContainer.Current 中,這樣,外層仍將正常運行。它們如今纔在底層使用您所選擇的容器。實際上,在主 Entlib 項目中,咱們不提供任何容器配置程序(Unity 除外);咱們但願,社區將爲其餘容器實現配置程序。

第二種方法是直接使用 EnterpriseLibraryContainer.Current。可調用 GetInstance<T>() 以獲取任何 Enterprise Library 對象,該對象會提供一個配置程序。一樣,若是願意,也可在其後附一個其餘容器。

最後一種方法,您能夠直接使用所選容器。必須使用配置程序將 Entlib 配置引導到容器中,但若是要使用容器,則須要對其進行設置,這並非一個新要求。而後,將所需 Entlib 對象做爲依賴關係進行注入,便可正常運行。

如何評價咱們的工做?

回顧一下咱們的目標以及咱們的設計是否符合這些目標。

  1. 現有客戶端代碼沒必要僅因體系結構更改而更改。可要求從新編譯,但不可要求更改源代碼(固然,客戶端 API 能夠因其餘 緣由進行更改)。可處理內部 API 或可擴展 API。

    符合。原始 API 仍可正常運行。若是您不使用依賴關係注入,則不須要了解也不須要關心您的對象在底層是如何關聯的。

  2. 刪除冗餘對象建立管道。應只保留一種(而非兩種或更多)建立對象的方式。

    符合。代碼庫再也不使用 ObjectBuilder 堆棧;如今,一切都經過 TypeRegistration 和配置程序機制進行構建。每一個容器都須要一個配置程序。

  3. 不使用 DI 的客戶不該受在內部使用 DI 的 Entlib 的影響。

    符合。DI 不會本身出現,除非您但願它出現。

  4. 確實須要 DI 的客戶能夠選擇所需容器,而後從中獲取本身的對象和 Entlib 對象。

    符合。您可直接使用所選 DI 容器,也可在靜態外層以後使用它。

此外,咱們還實現了其餘一些優勢。簡化了 Entlib 代碼庫。咱們從原始實現中刪除了大約 200 個類。添加類型註冊進行重構以後,一共減小了大約 80 個類。此外,添加的類比刪除的類更簡單,明顯提升了總體結構的一致性,減小了移動部件或特殊狀況。

另外一個優點是,重構的版本比原始版本更快一些,初步的非正式評估顯示,性能提升了 10%。這些數字說明咱們的工做是有效的。原始代碼中的複雜性大多源於針對 ObjectBuilder 的緩慢實現須要進行一系列性能優化。大多數 DI 容器針對其常規性能進行了大量工做。經過在容器之上重建 Entlib,能夠利用這些性能優化工做,從而沒必要本身完成大量這類工做。隨着 Unity 和其餘容器向前發展和優化,Entlib 的速度會更快,而無需咱們完成大量工做。

可供其餘庫借鑑的經驗

Enterprise Library 是一個很好的庫示例,它真正利用依賴關係注入容器,而不會與這種容器緊密耦合。若是要編寫使用 DI 容器的庫,但不但願將本身的選擇強加給客戶,能夠借鑑咱們的設計思路。我認爲,咱們針對「更改」設立的目標,尤爲是最後兩個,全部 庫(而不只僅是 Entlib)做者都應將其考慮在內:

  • 不使用 DI 的客戶不該受在內部使用 DI 的 Entlib 的影響。
  • 確實須要 DI 的客戶能夠選擇所需容器,而後從中獲取本身的對象和 Entlib 對象。

設計庫時,須要考慮幾個問題。請務必考慮如下問題:

  • 庫採用什麼引導方式?客戶是否必須完成特定工做才能設置您的代碼,或者,您是否有可正常運行的靜態入口點?
  • 您如何對對象圖進行建模,以便在配置容器時無需將調用硬編碼到該容器中?請參考咱們的 TypeRegistration 系統,尋找解決方法。
  • 如何管理要使用的容器?是在內部處理,仍是由調用方進行管理?調用方如何通知您要使用哪一個容器?

在咱們的項目中,咱們總結出了一整套很好的答案。但願咱們的示例能爲您的設計提供幫助。

 


 

Chris Tavares 是 Microsoft 模式和實施方案組的開發人員,在該組中,他任 Enterprise Library 和 Unity 項目的開發主管。在 Microsoft 就任以前,他曾從事諮詢、壓縮包裝軟件和嵌入式系統的工做。他在博客中發表了 Entlib、p&p 和常規開發方面的文章,網址爲:tavaresstudios.com

 

轉載自:http://msdn.microsoft.com/zh-cn/magazine/ee335709.aspx