這篇文章是我在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 的進一步發展,客戶也能夠更方便地對它進行了解、自定義和擴展。函數
咱們知道,有些重要方面須要改進,其中之一是對象建立管道。保留兩個並行但不一樣的代碼集實現同一功能會後患無窮。必須改變這種狀況。性能
咱們制定瞭如下重構目標:
不管從單獨仍是組合的角度來說,這些目標都意味着要進行大量工做。從表面看,「一個對象建立管道」目標至關簡單。咱們決定徹底刪除基於 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 對象做爲依賴關係進行注入,便可正常運行。
回顧一下咱們的目標以及咱們的設計是否符合這些目標。
現有客戶端代碼沒必要僅因體系結構更改而更改。可要求從新編譯,但不可要求更改源代碼(固然,客戶端 API 能夠因其餘 緣由進行更改)。可處理內部 API 或可擴展 API。
符合。原始 API 仍可正常運行。若是您不使用依賴關係注入,則不須要了解也不須要關心您的對象在底層是如何關聯的。
刪除冗餘對象建立管道。應只保留一種(而非兩種或更多)建立對象的方式。
符合。代碼庫再也不使用 ObjectBuilder 堆棧;如今,一切都經過 TypeRegistration 和配置程序機制進行構建。每一個容器都須要一個配置程序。
不使用 DI 的客戶不該受在內部使用 DI 的 Entlib 的影響。
符合。DI 不會本身出現,除非您但願它出現。
確實須要 DI 的客戶能夠選擇所需容器,而後從中獲取本身的對象和 Entlib 對象。
符合。您可直接使用所選 DI 容器,也可在靜態外層以後使用它。
此外,咱們還實現了其餘一些優勢。簡化了 Entlib 代碼庫。咱們從原始實現中刪除了大約 200 個類。添加類型註冊進行重構以後,一共減小了大約 80 個類。此外,添加的類比刪除的類更簡單,明顯提升了總體結構的一致性,減小了移動部件或特殊狀況。
另外一個優點是,重構的版本比原始版本更快一些,初步的非正式評估顯示,性能提升了 10%。這些數字說明咱們的工做是有效的。原始代碼中的複雜性大多源於針對 ObjectBuilder 的緩慢實現須要進行一系列性能優化。大多數 DI 容器針對其常規性能進行了大量工做。經過在容器之上重建 Entlib,能夠利用這些性能優化工做,從而沒必要本身完成大量這類工做。隨着 Unity 和其餘容器向前發展和優化,Entlib 的速度會更快,而無需咱們完成大量工做。
Enterprise Library 是一個很好的庫示例,它真正利用依賴關係注入容器,而不會與這種容器緊密耦合。若是要編寫使用 DI 容器的庫,但不但願將本身的選擇強加給客戶,能夠借鑑咱們的設計思路。我認爲,咱們針對「更改」設立的目標,尤爲是最後兩個,全部 庫(而不只僅是 Entlib)做者都應將其考慮在內:
設計庫時,須要考慮幾個問題。請務必考慮如下問題:
在咱們的項目中,咱們總結出了一整套很好的答案。但願咱們的示例能爲您的設計提供幫助。
Chris Tavares 是 Microsoft 模式和實施方案組的開發人員,在該組中,他任 Enterprise Library 和 Unity 項目的開發主管。在 Microsoft 就任以前,他曾從事諮詢、壓縮包裝軟件和嵌入式系統的工做。他在博客中發表了 Entlib、p&p 和常規開發方面的文章,網址爲:tavaresstudios.com。