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 學習資源整理

 

以上

4 則留言:

  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() 裡去增加註冊的步驟。

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

      刪除

提醒

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

最近的留言