天天看點

淺談依賴注入

最近幾天在看一本名為Dependency Injection in .NET 的書,主要講了什麼是依賴注入,使用依賴注入的優點,以及.NET平台上依賴注入的各種架構和用法。在這本書的開頭,講述了軟體工程中的一個重要的理念就是關注分離(Separation of concern, SoC)。依賴注入不是目的,它是一系列工具和手段,最終的目的是幫助我們開發出松散耦合(loose coupled)、可維護、可測試的代碼和程式。這條原則的做法是大家熟知的面向接口,或者說是面向抽象程式設計。

關于什麼是依賴注入,在Stack Overflow上面有一個問題,如何向一個5歲的小孩解釋依賴注入,其中得分最高的一個答案是:

“When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn’t want you to have. You might even be looking for something we don’t even have or which has expired.

What you should be doing is stating a need, “I need something to drink with lunch,” and then we will make sure you have something when you sit down to eat.”

映射到面向對象程式開發中就是:高層類(5歲小孩)應該依賴底層基礎設施(家長)來提供必要的服務。

編寫松耦合的代碼說起來很簡單,但是實際上寫着寫着就變成了緊耦合。

使用例子來說明可能更簡潔明了,首先來看看什麼樣的代碼是緊耦合。

1 不好的實作

編寫松耦合代碼的第一步,可能大家都熟悉,那就是對系統分層。比如下面的經典的三層架構。

淺談依賴注入

分完層和實作好是兩件事情,并不是說分好層之後就能夠松耦合了。

有很多種方式來設計一個靈活的,可維護的複雜應用,但是n層架構是一種大家比較熟悉的方式,這裡面的挑戰在于如何正确的實作n層架構。

假設要實作一個很簡單的電子商務網站,要列出商品清單,如下:

淺談依賴注入

下面就具體來示範通常的做法,是如何一步一步把代碼寫出緊耦合的。

要實作商品清單這一功能,首先要編寫資料通路層,需要設計資料庫及表,在SQLServer中設計的資料庫表Product結構如下:

淺談依賴注入

表設計好之後,就可以開始寫代碼了。在Visual Studio 中,建立一個名為DataAccessLayer的工程,添加一個ADO.NET Entity Data Model,此時Visual Studio的向導會自動幫我們生成Product實體和ObjectContext DB操作上下文。這樣我們的 Data Access Layer就寫好了。

表現層實際上可以直接通路資料通路層,通過ObjectContext 擷取Product 清單。但是大多數情況下,我們不是直接把DB裡面的資料展現出來,而是需要對資料進行處理,比如對會員,需要對某些商品的價格打折。這樣我們就需要業務邏輯層,來處理這些與具體業務邏輯相關的事情。

建立一個類庫,命名為DomainLogic,然後添加一個名為ProductService的類:

現在我們的業務邏輯層已經實作了。

現在實作表現層邏輯,這裡使用ASP.NET MVC,在Index 頁面的Controller中,擷取商品清單然後将資料傳回給View。

然後在View中将Controller中傳回的資料展現出來:

現在,按照三層“架構”我們的代碼寫好了,并且也達到了要求。整個項目的結構如下圖:

淺談依賴注入

這應該是我們通常經常寫的所謂的三層架構。在Visual Studio中,三層之間的依賴可以通過項目引用表現出來。

現在我們來分析一下,這三層之間的依賴關系,很明顯,上面的實作中,DomianLogic需要依賴SqlDataAccess,因為DomainLogic中用到了Product這一實體,而這個實體是定義在DataAccess這一層的。WebUI這一層需要依賴DomainLogic,因為ProductService在這一層,同時,還需要依賴DataAccess,因為在UI中也用到了Product實體,現在整個系統的依賴關系是這樣的:

淺談依賴注入

使用三層結構的主要目的是分離關注點,當然還有一個原因是可測試性。我們應該将領域模型從資料通路層和表現層中分離出來,這樣這兩個層的變化才不會污染領域模型。在大的系統中,這點很重要,這樣才能将系統中的不同部分隔離開來。

現在來看之前的實作中,有沒有子產品性,有沒有那個子產品可以隔離出來呢。現在添加幾個新的case來看,系統是否能夠響應這些需求:

添加新的使用者界面

除了WebForm使用者之外,可能還需要一個WinForm的界面,現在我們能否複用領域層和資料通路層呢?從依賴圖中可以看到,沒有任何一個子產品會依賴表現層,是以很容易實作這一點變化。我們隻需要建立一個WPF的富用戶端就可以。現在整個系統的依賴圖如下:

淺談依賴注入

更換新的資料源

可能過了一段時間,需要把整個系統部署到雲上,要使用其他的資料存儲技術,比如Azure Table Storage Service。現在,整個通路資料的協定發生了變化,通路Azure Table Storage Service的方式是Http協定,而之前的大多數.NET 通路資料的方式都是基于ADO.NET 的方式。并且資料源的儲存方式也發生了改變,之前是關系型資料庫,現在變成了key-value型資料庫。

淺談依賴注入

由上面的依賴關系圖可以看出,所有的層都依賴了資料通路層,如果修改資料通路層,則領域邏輯層,和表現層都需要進行相應的修改。

除了上面的各層之間耦合下過強之外,代碼中還有其他問題。

領域模型似乎都寫到了資料通路層中。是以領域模型看起來依賴了資料通路層。在資料通路層中定義了名為Product的類,這種類應該是屬于領域模型層的。

表現層中摻入了決定某個使用者是否是會員的邏輯。這種業務邏輯應該是 業務邏輯層中應該處理的,是以也應該放到領域模型層

ProductService因為依賴了資料通路層,是以也會依賴在web.config 中配置的資料庫連接配接字元串等資訊。這使得,整個業務邏輯層也需要依賴這些配置才能正常運作。

在View中,包含了太多了函數性功能。他執行了強制類型轉換,字元串格式化等操作,這些功能應該是在界面顯示得模型中完成。

上面可能是我們大多數寫代碼時候的實作, UI界面層去依賴了資料通路層,有時候偷懶就直接引用了這一層,因為實體定義在裡面了。業務邏輯層也是依賴資料通路層,直接在業務邏輯裡面使用了資料通路層裡面的實體。這樣使得整個系統緊耦合,并且可測試性差。那現在我們看看,如何修改這樣一個系統,使之達到松散耦合,進而提高可測試性呢?

2 較好的實作

依賴注入能夠較好的解決上面出現的問題,現在可以使用這一思想來重新實作前面的系統。之是以重新實作是因為,前面的實作在一開始的似乎就沒有考慮到擴充性和松耦合,使用重構的方式很難達到理想的效果。對于小的系統來說可能還可以,但是對于一個大型的系統,應該是比較困難的。

在寫代碼的時候,要管理好依賴性,在前面的實作這種,代碼直接控制了依賴性:當ProductService需要一個ObjectContext類的似乎,直接new了一個,當HomeController需要一個ProductService的時候,直接new了一個,這樣看起來很酷很友善,實際上使得整個系統具有很大的局限性,變得緊耦合。new 操作實際上就引入了依賴, 控制反轉這種思想就是要使的我們比較好的管理依賴。

首先從表現層來分析,表現層主要是用來對資料進行展現,不應該包含過多的邏輯。在Index的View頁面中,代碼希望可以寫成這樣

可以看出,跟之前的表現層代碼相比,要整潔很多。很明顯是不需要進行類型轉換,要實作這樣的目的,隻需要讓Index.aspx這個視圖繼承自 System.Web.Mvc.ViewPage<FeaturedProductsViewModel> 即可,當我們在從Controller建立View的時候,可以進行選擇,然後會自動生成。整個用于展示的資訊放在了SummaryText字段中。

這裡就引入了一個視圖模型(View-Specific Models),他封裝了視圖的行為,這些模型隻是簡單的POCOs對象(Plain Old CLR Objects)。FeatureProductsViewModel中包含了一個List清單,每個元素是一個ProductViewModel類,其中定義了一些簡單的用于資料展示的字段。

淺談依賴注入

現在在Controller中,我們隻需要給View傳回FeatureProductsViewModel對象即可。比如:

現在傳回的是空清單,具體的填充方式在領域模型中,我們接着看領域模型層。

建立一個類庫,這裡面包含POCOs和一些抽象類型。POCOs用來對領域模組化,抽象類型提供抽象作為到達領域模型的入口。依賴注入的原則是面向接口而不是具體的類程式設計,使得我們可以替換具體實作。

現在我們需要為表現層提供資料。是以使用者界面層需要引用領域模型層。對資料通路層的簡單抽象可以采用Patterns of Enterprise Application Architecture一書中講到的Repository模式。是以定義一個ProductRepository抽象類,注意是抽象類,在領域模型庫中。它定義了一個擷取所有特價商品的抽象方法:

這個方法的Product類中隻定義了商品的基本資訊比如名稱和單價。整個關系圖如下:

淺談依賴注入

現在來看表現層,HomeController中的Index方法應該要使用ProductService執行個體類來擷取商品清單,執行價格打折,并且把Product類似轉化為ProductViewModel執行個體,并将該執行個體加入到FeaturesProductsViewModel中。因為ProductService有一個帶有類型為ProductReposity抽象類的構造函數,是以這裡可以通過構造函數注入實作了ProductReposity抽象類的執行個體。這裡和之前的最大差別是,我們沒有使用new關鍵字來立即new一個對象,而是通過構造函數的方式傳入具體的實作。

現在來看表現層代碼:

在HomeController的構造函數中,傳入了實作了ProductRepository抽象類的一個執行個體,然後将該執行個體儲存在定義的私有的隻讀的ProductRepository類型的repository對象中,這就是典型的通過構造函數注入。在Index方法中,擷取資料的ProductService類中的主要功能,實際上是通過傳入的repository類來代理完成的。

ProductService類是一個純粹的領域對象,實作如下:

可以看到ProductService也是通過構造函數注入的方式,儲存了實作了ProductReposity抽象類的執行個體,然後借助該執行個體中的GetFeatureProducts方法,擷取原始清單資料,然後進行打折處理,進而實作了自己的GetFeaturedProducts方法。在該GetFeaturedProducts方法中,跟之前不同的地方在于,現在的參數是IPrincipal,而不是之前的bool型,因為判斷使用者的狀況,這是一個業務邏輯,不應該在表現層處理。IPrincipal是BCL中的類型,是以不存在額外的依賴。我們應該基于接口程式設計IPrincipal是應用程式使用者的一種标準方式。

這裡将IPrincipal作為參數傳遞給某個方法,然後再裡面調用實作的方式是依賴注入中的方法注入的手段。和構造函數注入一樣,同樣是将内部實作代理給了傳入的依賴對象。

現在我們隻剩下兩塊地方沒有處理了:

沒有ProductRepository的具體實作,這個很容易實作,後面放到資料通路層裡面去處理,我們隻需要建立一個具體的實作了ProductRepository的資料通路類即可。

預設上,ASP.NET MVC 希望Controller對象有自己的預設構造函數,因為我們在HomeController中添加了新的構造函數來注入依賴,是以MVC架構不知道如何解決建立執行個體,因為有依賴。這個問題可以通過開發一個IControllerFactory來解決,該對象可以建立一個具體的ProductRepositry執行個體,然後傳給HomeController這裡不多講。

現在我們的領域邏輯層已經寫好了。在該層,我們隻操作領域模型對象,以及.NET BCL 中的基本對象。模型使用POCOs來表示,命名為Product。領域模型層必須能夠和外界進行交流(database),是以需要一個抽象類(Repository)來時完成這一功能,并且在必要的時候,可以替換具體實作。

現在我們可以使用LINQ to Entity來實作具體的資料通路層邏輯了。因為要實作領域模型的ProductRepository抽象類,是以需要引入領域模型層。注意,這裡的依賴變成了資料通路層依賴領域模型層。跟之前的恰好相反,代碼實作如下:

在這裡需要注意的是,在領域模型層中,我們定義了一個名為Product的領域模型,然後再資料通路層中Entity Framework幫我們也生成了一個名為Product的資料通路層實體,他是和db中的Product表一一對應的。是以我們在方法傳回的時候,需要把類型從db中的Product轉換為領域模型中的POCOs Product對象。

淺談依賴注入

Domain Model中的Product是一個POCOs類型的對象,他僅僅包含領域模型中需要用到的一些基本字段,DataAccess中的Product對象是映射到DB中的實體,它包含資料庫中Product表定義的所有字段,在資料表現層中我們 定義了一個ProductViewModel資料展現的Model。

這兩個對象之間的轉換很簡單:

現在,整個系統的依賴關系圖如下:

淺談依賴注入

表現層和資料通路層都依賴領域模型層,這樣,在前面的case中,如果我們新添加一個UI界面;更換一種資料源的存儲和擷取方式,隻需要修改對應層的代碼即可,領域模型層保持了穩定。

整個系統的時序圖如下:

淺談依賴注入

系統啟動的時候,在Global.asax中建立了一個自定義了Controller工廠類,應用程式将其儲存在本地便兩種,當頁面請求進來的時候,程式出發該工廠類的CreateController方法,并查找web.config中的資料庫連接配接字元串,将其傳遞給新的SqlProductRepository執行個體,然後将SqlProductRepository執行個體注入到HomeControll中,并傳回。

然後應用調用HomeController的執行個體方法Index來建立新的ProductService類,并通過構造函數傳入SqlProductRepository。ProductService的GetFeaturedProducts 方法代理給SqlProductRepository執行個體去實作。

最後,傳回填充好了FeaturedProductViewModel的ViewResult對象給頁面,然後MVC進行合适的展現。

在1.1的實作中,采用了三層架構,在改進後的實作中,在UI層和領域模型層中加入了一個表現模型(presentation model)層。如下圖:

将Controllers和ViewModel從表現層移到了表現模型層,僅僅将視圖(.aspx和.ascx檔案)和聚合根對象(Composition Root)保留在了表現層中。之是以這樣處理,是可以使得盡可能的使得表現層能夠可配置而其他部分盡可能的可以保持不變。

3. 結語

一不小心我們就編寫出了緊耦合的代碼,有時候以為分層了就可以解決這一問題,但是大多數的時候,都沒有正确的實作分層。之是以容易寫出緊耦合的代碼有一個原因是因為程式設計語言或者開發環境允許我們隻要需要一個新的執行個體對象,就可以使用new關鍵字來執行個體化一個。如果我們需要添加依賴,Visual Studio有些時候可以自動幫我們添加引用。這使得我們很容易就犯錯,使用new關鍵字,就可能會引入以來;添加引用就會産生依賴。

減少new引入的依賴及緊耦合最好的方式是使用構造函數注入依賴這種設計模式:即如果我們需要一個依賴的執行個體,通過構造函數注入。在第二個部分的實作示範了如何針對抽象而不是具體程式設計。

構造函數注入是反轉控制的一個例子,因為我們反轉了對依賴的控制。不是使用new關鍵字建立一個執行個體,而是将這種行為委托給了第三方實作。

希望本文能夠給大家了解如何真正實作三層架構,編寫松散耦合,可維護,可測試性的代碼提供一些幫助。

繼續閱讀