2013年4月14日 星期日

ASP.NET MVC 專案分層架構 Part.6 - DI/IoC 使用 Unity.MVC

2014-12-02 補充說明:
這一系列的文章並不適合初階及中階的開發人員,如果你是程式開發的初學者或是 ASP.NET MVC 初學者,甚至是開發經驗少於兩年的開發人員,請馬上離開此篇文章。

隔了好長一段時間沒有接續這個系列的文章,現在直接從 DI/IoC  來繼續專案分層架構的主題,有關在專案裡實作 DI/IoC 的 Framework 有相當多,例如:Autofac, Castle Windsor, StructureMap, Ninject, Simple Injector, Enterprise Library Unity Application Block 等,其實還有很多的 Framework 可以使用,但這邊並不是探討哪一種 Framework 的優劣,這些 DI/IoC Framework 都相當不錯而且也有很多人使用,在網路上也可以找到豐富的文件以及參考文章,而這篇文章則是使用「Unity.MVC4」。

DI:Dependency Injection 依賴注入.
IoC:Inversion of Control 控制反轉.

P.S.
其他 DI/IoC Framework 的使用則會另開文章來做介紹,就不是隸屬於「分層架構」的分類中。

 


我們在「ASP.NET MVC 專案分層架構 Part.1 ~ Part.5」已經將專案從原本的單一專案逐漸拆分為三個專案,各個專案都有其專有的職責,在 Repository 以及 Service 專案中也都有建立各類別的 Interface,建立介面可以避免讓程式去直接依賴實作類別。

下面的程式碼,就是一種直接依賴,

image

image

 

在專案架構裡,我們都會強調所謂的彈性,如果少了彈性就代表系統缺乏可擴充與變化性,舉一個很明顯的例子就是,專案一開始所使用的資料庫是 MS SQL Server,所以專案中的資料存取都是完全以存取 MS SQL Server 的方式來實作,假如某天這個專案想要產品化,但是客戶只有 Oracle 並且不想多花錢去買 MS SQL Server,這時候該怎麼辦呢?

最常見的處理方式就是這類型的專案會有因應多種資料庫不同的版本產生,或者就是直接將程式碼中原本對 MS SQL Server 的存取處理給硬生生修改為存取 Oracle 的內容;其實無論哪一種方法都相當耗費時間與精神,以第一種方式來說,同樣的程式碼只因為資料庫的不同就會多個版本出現,會造成日後更新維護上的困難。

「程式的內容是針對介面而寫,而不是針對實作而寫」

雖然之前的程式碼還是直接依賴於實作的類別,但之前我們有建立了介面,也就保持了彈性,像上面所提到的狀況,不同資料庫的資料存取程式就會是依照介面所制定的內容來實作,那麼系統日後假如要更換資料庫時就不需要重新改寫程式或是又多一種資料庫版本的專案,不過這還是存在一個問題,怎麼讓程式知道更換了不同的實作程式呢?

我們有建立了介面,而不同的資料庫存取操作方式都是依照著介面來實作的,這時候就可以在專案中導入 DI/IoC Framework 來解決這部份的問題。

「物件反轉又稱為依賴注入,在物件導向設計中,一個用來降低物件之間耦合性的設計原則」
- from ASP.net MVC 4 網站開發美學 Ch.8 - 2

想要深入了解 DI/IoC 的定義與說明,建議大家可以參閱「ASP.net MVC 4 網站開發美學 Chapter 8-2」,或是也可以參考以下的文章:

Inversion of Control Containers and the Dependency Injection pattern - Martin Fowler

控制反轉(Inversion od Control)依賴注入(Dependency Injection)- Wiki

[Software Architecture]IoC and DI - In 91- 點部落

[ASP.NET]重構之路系列v4 – 簡單使用interface之『你也會IoC』 - In 91- 點部落

 


而這邊我們所使用的 DI/IoC Solution 為「Enterprise Library Unity Application Block」, Enterprise Library 是微軟針對企業應用所推出的 Library,不是用來取代原本的 .NET Framework,而是提供了輔助與加強的方式可以用於系統開發之中,在 MSDN 上有相當多的文件可供學習與參考,而 Unity Application Block 為 EntLib 其中一個 Application Block,而使用 EntLib Unity 時並不需要把整個 EntLib 都給專案參考,只要加入需要的組件即可。

ASP.NET MVC 專案要使用 Unity Application Block 並不需要完全手動加入,可以藉由 Visual Studio 的 NuGet 來取得,在 NuGet 上可以找到並且已經整合好給 ASP.NET MVC 專案使用的組件,這次我們所要用的是「Unity.MVC4」(各位可以依據自己專案的版本來選取適當的組件來使用)。

SNAGHTML9a6f25c

 

Unity.MVC4

http://nuget.org/packages/Unity.Mvc4/

image

 

Web 專案透過 NuGet 加入 Unity.MVC4 組件參考

在 Web 專案上選擇「管理 NuGet 套件」,並且在 NuGet 管理視窗中尋找 Unity.MVC4 然後安裝,

SNAGHTML9ac5810

安裝完成之後會在 Web 專案的根目錄下新增一個「Bootstrapper.cs」檔案,

image

Bootstrapper.cs

using System.Web.Mvc;
using Microsoft.Practices.Unity;
using Unity.Mvc4;
 
namespace Mvc_Repository.Web
{
    public static class Bootstrapper
    {
        public static IUnityContainer Initialise()
        {
            var container = BuildUnityContainer();
 
            DependencyResolver.SetResolver(new UnityDependencyResolver(container));
 
            return container;
        }
 
        private static IUnityContainer BuildUnityContainer()
        {
            var container = new UnityContainer();
 
            // register all your components with the container here
            // it is NOT necessary to register your controllers
 
            // e.g. container.RegisterType<ITestService, TestService>();    
            RegisterTypes(container);
 
            return container;
        }
 
        public static void RegisterTypes(IUnityContainer container)
        {
 
        }
    }
}

上面是預設加入的內容,我們還需要動手做修改,而其中的 RegisterTypes() 方法就是我們要加入程式的地方,接下來我們要先調整 Service 與 Web 專案的程式內容。

 

Service 專案

CategoryService.cs

image

這邊原先的作法就是直接在程式中去建立一個 GenericRepository<Categories> 的實例,而現在要解除這種直接依賴的關係,而往後建立實例的工作就交由 Unity 來處理,所以這裡就要做修改,在 CategoryService 裡增加一個含有 IRepository<Categories> 參數的建構式,

image

同樣的在 ProductService.cs 也是做同樣的修改,

ProductService.cs

image

 

Web 專案

Service 層的資料存取是透過 Model 層,而 Web 專案的商業邏輯操作則是透過 Service 層,所以 Web 專案裡的有使用到 Service 的地方也要做修改,

CategoryController.cs

原本的 CategoryController 內容,

image

按照 Service 的修改方式,修改如下:

image

ProductController.cs

修改後的內容,

image

 

Unity.MVC 的 Bootstrapper.cs 修改

完成 Service 專案以及 Web 專案內直接依賴的地方修改過後,再來就是到 Bootstrapper.cs 的 RegisterTypes 方法內去增加註冊的介面類別以及要實作的類別,一開始加入的 Bootstrapper.cs 內就已經告訴我們要怎麼增加了,

image

所以在 RegisterTypes 方法裡增加了以下的內容,

image

Bootstrapper.cs

using System.Web.Mvc;
using Microsoft.Practices.Unity;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
using Mvc_Repository.Models.Repository;
using Mvc_Repository.Service;
using Mvc_Repository.Service.Interface;
using Unity.Mvc4;
 
namespace Mvc_Repository.Web
{
    public static class Bootstrapper
    {
        public static IUnityContainer Initialise()
        {
            var container = BuildUnityContainer();
 
            DependencyResolver.SetResolver(new UnityDependencyResolver(container));
 
            return container;
        }
 
        private static IUnityContainer BuildUnityContainer()
        {
            var container = new UnityContainer();
 
            RegisterTypes(container);
 
            return container;
        }
 
        public static void RegisterTypes(IUnityContainer container)
        {
            // Repository
            container.RegisterType<IRepository<Categories>, GenericRepository<Categories>>();
            container.RegisterType<IRepository<Products>, GenericRepository<Products>>();
 
            // Service
            container.RegisterType<ICategoryService, CategoryService>();
            container.RegisterType<IProductService, ProductService>();
 
        }
    }
}

最後再到 Global.asax 的 Application_Start() 方法裡增加呼叫 Bootstrapper.Initialise() 方法,

image

到這邊就完成了這次專案加入使用 Unity.MVC4 的修改。

 

2013-04-27 更新:

我必須說遇到了很奇怪的狀況,我每一篇文章的程式範例都是在可以確定正常運作的情況下所寫出來的,但是就在寫「ASP.NET MVC 4 使用 Unity bootstrapper for ASP.NET MVC」的時候就遇到了問題,所以就決定將原本這篇文章的程式做了修改,並且也把修正的地方與內容做說明。

當我們完成了 Unity.MVC4 的整合與設定之後,大致上工作有已經完成了,但我卻遇到了以下的錯誤,

image

看來是 GenericRepository 的建構式出了問題。

 

GenericRepository.cs

這是 GenericRepository.cs 檔案原本的內容,這邊只擷取前面建構式的地方,

image

在 GenericRepository.cs 裡有兩個建構式是有傳入參數的,雖然說兩個傳入參數的型別不同,但是對於 Unity 來說,這兩個沒辦法去分別呀,因為建構式的傳入參數個數都一樣,在 Unity 去做型別註冊時就根本不知道該用哪一個建構式,所以先刪除另一個以 ObjectContext 為輸入參數的建構式,

image

但是做了上面的修改後去執行網站卻會發現以下的錯誤,

image

於是我就把 GenericRepository 做了修改,只留下一個建構式,

image

而 GenericRepository 建構式所需要傳入的 DbContext 就需要在 Unity 於建立類別實例的時就要給,而這部份的工作就要在 Bootstrapper.cs 的 RegisterTypes() 方法內來完成,而怎麼在註冊型別的時候讓 Unity 知道要傳什麼參數值給指定類別的建構式呢?

這邊我們要使用的是  InjectionConstructor,如下:

image

修改的程式內容:

image

經過上面的修改過後,網站就可以正常運作了。

image

 


本來我們在各個類別內部直接對於要使用的 Service or Repository 去建立一個實例(instance),但這樣的作法就是我們所說的依賴了某個物件,而透過建立介面,然後原本內部直接去相依物件類別的地方修改為不直接相依,而是透過介面,最後再由外部去決定這個介面要實作哪一個類別,而這個外部就是我們這一篇所使用的 Unity.MVC。

藉由這樣的改變,讓原本 Service 層一定要某個 Repository 物件或是 Controller 一定要某個 Service 的地方可以解除直接依賴的關係,在日後系統做調整或是抽換的時候就不用大費周章地修改程式或是再去重複建立一個相同結構的專案,這也就是我們一開始所強調的彈性。

這一篇並未深入探討有關「依賴注入/控制反轉」的內容與細節,主要還是定義閱讀這一系列內容的對象是分層架構的初學者或是想要入門的朋友,不希望有太多艱深的名詞以及觀念而讓有些朋友卻步,先從直接的實作著手開始,然後在實作的過程中或是完成實作之後再去思考,否則我這邊講再多、再深入而讓閱讀的朋友沒有辦法應用在實作上,那我想這就浪費了閱讀這篇文章的時間了。

 

補充:

如果想對 DI(Dependency Injection)有更深入的了解的朋友,可以前往蔡煥麟老師的部落格,有一系列深入介紹的文章,值得前往細細閱讀。

Huan-Lin 學習筆記: Dependency Injection 筆記 (1)
Huan-Lin 學習筆記: Dependency Injection 筆記 (2)
Huan-Lin 學習筆記: Dependency Injection 筆記 (3)
Huan-Lin 學習筆記: Dependency Injection 筆記 (4)
Huan-Lin 學習筆記: Dependency Injection 筆記 (5)
Huan-Lin 學習筆記: Dependency Injection 筆記 (6)

 

系列文章下一篇:

ASP.NET MVC 專案分層架構 - 建議與補充說明

錯誤更正:有關 ASP.NET MVC 分層架構使用 Unity 的 DbContext 處理

 

參考連結:

Inversion of Control Containers and the Dependency Injection pattern - Martin Fowler

控制反轉(Inversion od Control)依賴注入(Dependency Injection)- Wiki

[Software Architecture]IoC and DI - In 91- 點部落

[ASP.NET]重構之路系列v4 &ndash; 簡單使用interface之『你也會IoC』 - In 91- 點部落

[Object-oriented] 控制反轉 - 昏睡領域- 點部落

The Will Will Web | Unity Application Block 與 ASP.NET MVC 學習資源整理

 

以上

14 則留言:

  1. 您好:
    參考您的ASP.NET MVC 專案分層架構第一篇到的第六篇
    也實作了相關程式。
    遇到了一個問題。
    此篇文章有提到:GenericRepository 建構式所需要傳入的 DbContext 就需要在 Unity 於建立類別實例的時就要給。
    雖然新增/修改/刪除/查詢功能都正常,但我發現程式執行中時,如果直接去資料庫改資料,從Service讀出來的資料都還是舊的。應該是App_Start之後,都是共用同一個DbContext造成的。
    不知道有什麽方式可以解決?

    回覆刪除
    回覆
    1. 其實這個跟是否共用同一個 DbContext 沒有關係,既使是用最傳統的方式(沒有做分層,在每個 Controller 都各自建立 Entities)也是會有相同的狀況,這是因為 Entity Framework 在存取資料時會有一套機制,會暫存資料在系統中,當有透過 EF 進行資料變動時會去更新 DbContext 內的狀態,然後當有讀取資料的需求時就會先看 DbContext 有無讀取過該筆資料,如果有而且沒有修改過,就會直接把料給送出去。
      而你所謂直接去修改資料庫中的資料,因為自己去修改資料,並不會通知 EF,所以就會出現取得的資料還是之前的狀況,跟是否用同一個 DbContext 無關。

      刪除
    2. 謝謝回覆,了解。
      那如果系統透過EF讀取資料,此時透過第三方修改資料時,有甚麼方式可以解決EF讀取先前資料的問題嗎?
      我想到的只有每次去讀資料時,重新 new DbContext,重新抓資料。 = ="

      刪除
    3. 有關這個部分的問題,我有找出原因,這也是當初我寫的時候沒有多加注意而衍生的一個錯誤,
      因為 RegisterTypes() 這個方法是 static 的,而在 ststic 裡去做建立一個 DbContext 實例的動作,
      我這麼做就產生了問題,也就發生了你所說的狀況,這個我會在找個時間做個補充說明,
      先簡短的說明一下,我們要做的修正就是把建立 DbContext 實例的步驟給移出 RegisterTypes() 靜態方法之外,
      然後另外建立一個 IDbContextFactory 與 DbContextFactory,DbContextFactory 要做的事情就只有建立 DbContext 實例而已,
      最後再到 RegisterTypes() 裡去增加註冊的步驟。

      最後也感謝你回應我這樣訊息。

      刪除
  2. 你好,想請問 Dependency Injection(DI) 與 Service Location(SL) 應用的問題,
    在工作上,由較資深的前輩定義專案架構,其中有使用了 DI 在建構式注入,
    但資深前輩在建構式當中注入的是一個類似 SL 的 Interface,之後使用該 SL 物件取得服務物件,
    說這樣就不用宣告一堆物件參數在建構式,某個 Method 想用的時候,就 SL.GetService(),
    一開始跟著使用覺得好像也沒什麼問題,但我看見一篇文章
    https://stackoverflow.com/questions/4985455/dependency-injection-vs-service-location
    指出 DI + SL 同時使用是不建議,英文我沒能全理解,
    故來詢問有經驗的前輩,這麼做是良好的作法,或者是隱藏著什麼問題?

    回覆刪除
    回覆
    1. 你所使用的 DI Framwork 是哪一個呢?而你所說的「類似 SL 的 Interface」是什麼呢?
      我是覺得你應該將 Service Locator 的一些方法與 DI 的 Resolve 方法給混為一談了
      應該是先理解你所使用的 DI Framework
      一般而言,現在的開發者與開發的專案不會去直接使用到 Service Locator 才是

      回到 DI 的這部分來說,還是建議使用顯式的注入,例如建構式注入以及屬性注入,
      而只有在無法使用建構式注入或屬性注入的情況下才會使用 DI 的 Resolve 方法去取得實作類別
      大量的使用 Resolve 會有效能的問題,另一種就是因為「懶」或是貪圖方便,這會有之後維護上的問題,
      類別使用相依型別,這是要好好考慮的,類別的設計應該要遵循 SOLID 原則
      大量使用 Resolve 就容易破壞 SRP 單一職責,容易讓類別變成雜七雜八的超級類別(有人稱為上帝類別)
      後續的維護就會是個大麻煩

      何謂 Service Locator pattern
      http://www.cnblogs.com/gaochundong/archive/2013/04/12/service_locator_pattern.html

      P&P Unity 這套 DI Framework 所提供的 Resolve 方法
      https://msdn.microsoft.com/en-us/library/ff664762(v=pandp.50).aspx

      刪除
    2. 感謝大大回覆!
      DI Fraemwork 目前是用 ASP.NET Core 內建的 DI,
      類似 SL 功能的 Interface 我沒描述清楚,就是直接使用 DI 的 Resolve,
      只是建立了一個 DI Interface ,實作內容就是使用內建 DI 的 Resolve,
      然後應用上直接注入 DI Interface的實作到類別的建構子參數中,
      之後方法裡直接使用該 DI Interface 的 Resolve 在需要的時候取得物件,
      這種方式,讓我聯想到直接呼叫 SL 取得物件的功用類似感覺相同,
      目前我覺得這方法除了懶惰可以少寫幾行代碼外,目前沒想到其他好處,
      原本單元測試,我習慣在建構式就能看出所需要的哪些Interface,
      現在因專案架構這樣做,單元測試額外就得多弄那個DI Interface,
      但這些都不足影響資深前輩的想法,
      大大回覆中提到『大量使用 Resolve 就容易破壞 SRP 單一職責,容易讓類別變成雜七雜八的超級類別』,
      因為我沒直接使用 Resolve的後期經驗,很難搬出一個案例去跟專案資深前輩討論,
      請問大大有這種案例參考嗎?

      刪除
    3. 把類別依據職責去做設計時,一開始類別會用到哪些相依的類別就會制訂好,
      指定好類別會有哪些屬性,建構式就會一開始就指定好引數是要做哪些相依類別的注入
      在實作方法的時候就不會超出職責太多

      然而一旦想要在原本設計好的類別裡去使用到原本沒有指定的相依類別時,
      在合理的職責範圍裡,依據設計習慣,其實就是增加屬性,架構式增加引數
      但其實不管是不是在合理的職責範圍裡,都是可以透過上面的作法去增加類別的相依類別
      不過至少在後續維護的時候,在類別的建構式以及屬性就可以很快的看出該類別還有與哪些類別有相依

      如果大量的在方法裡直接去使用 Resolve 的方式,而不是透過類別一開始所制訂的屬性取得相依類別
      無疑就是開了方便大門,不管是不是在職責裡的功能,反正就是用 Resolve 直接取得
      長久下來,這個類別相依了多少類別就很難一眼看出,後續的維護就會很累
      而且大量的使用 Resolve 方式,不就等於直接在方法裡去建立某類別的實體嗎(new Class1())
      這樣就失去了使用 DI 的意義

      越來越多人直接開發 ASP.NET Core 專案,因為 asp.net core 一開始就提供了 DI,所以就會讓開發者要直接面對
      在以往的 ASP.NET WebForm 與 ASP.NET MVC WebApi 專案,還是有很多開發者寫了多少年的程式卻連 DI 也沒用過
      甚至連聽都沒聽過,也別說有這概念,既使有用過,但是很多都用偏了
      所以就會有大量去直接使用 Resolve 的方式去偏廢了原本應該要用的建構式注入或屬性注入的作法
      Resolve 方式不是不能夠用,而是需要正確的使用

      刪除
    4. 現實總是殘酷,當你資歷是最菜的,3個都是前輩且還有資深都認為直接 Resolve 更方便時,
      這些理論還撼動不了他們的想法,我想只有等待技術債來臨或有權威的人才能改變作法,
      此時的我能力不足,就算知道這更良好的做法,但總是被反駁,只能感謝大大幫我上了一堂課。

      刪除
  3. 想問一問雖然 Class CategoryService.cs 中改以 CategoryService function 中以 IRepository parameter 以降低直接依賴, 但其實IRepository 當中的Generic Class Categories會對IRepository 限制, 其實Generic class 對 interface 是否增加耦合性?

    回覆刪除
  4. 網誌管理員已經移除這則留言。

    回覆刪除
  5. 請問,可以講講同一個action裡,如何在一個transaction做到 兩個不同service 的insert/update/delete?
    類似
    using transaction
    {
    categoryService.Create(instance1);
    productService.Create(instance2);
    }

    回覆刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。