MVC 5 + EF6 完整教程15 -- 使用DI進行解耦

MiroYuan發表於2017-04-06

如果大家研究一些開源專案,會發現無處不在的DI(Dependency Injection依賴注入)。
本篇文章將會詳細講述如何在MVC中使用Ninject實現DI

文章提綱

  • 場景描述 & 問題引出
  • 第一輪重構
  • 引入Ninject
  • 第二輪重構
  • 總結

場景描述 & 問題引出

DI是一種實現元件解耦的設計模式。
先模擬一個場景來引出問題,我們直接使用Ninject官網的示例:一群勇士為了榮耀而戰。
首先,我們需要一件合適的武器裝備這些勇士。

class Sword 
{
    public void Hit(string target)
    {
        Console.WriteLine("Chopped {0} clean in half", target);
    }
}

其次,我們定義勇士類。
勇士有一個Attack()方法,用來攻擊敵人。

class Samurai
{
    readonly Sword sword;
    public Samurai()
    {
        this.sword = new Sword();
    }
    
    public void Attack(string target)
    {
        this.sword.Hit(target);
    }
}

現在我們就可以建立一個勇士來戰鬥。

class Program
{
    public static void Main()
    {
        var warrior = new Samurai();
        warrior.Attack("the evildoers");
    }
}

我們執行這個程式就會列印出 Chopped the evildoers clean in half
現在引出我們的問題:如果我們想要給Samurai 裝備不同的武器呢?
由於 Sword 是在 Samurai 類的建構函式中建立的,必須要改 Samurai才行。
很顯然 Samurai 和 Sword 的耦合性太高了,我們先定義一個介面來解耦。

第一輪重構

首先需要建立鬆耦合元件:通過引入IWeapon,保證了Program與Sword之間沒有直接的依賴項。

interface IWeapon
{
    void Hit(string target);
}

MVC 5 + EF6 完整教程15 -- 使用DI進行解耦

修改 Sword 類

class Sword : IWeapon
{
    public void Hit(string target)
    {
        Console.WriteLine("Chopped {0} clean in half", target);
    }
}

修改 Samurai 類,將原來建構函式中的Sword 移到建構函式的引數上,以介面來代替 , 然後我們就可以通過 Samurai 的建構函式來注入 Sword ,這就是一個DI的例子(通過建構函式注入)。

class Samurai
{
    readonly IWeapon weapon;
    public Samurai(IWeapon weapon)
    {
        this.weapon = weapon;
    }
    
    public void Attack(string target)
    {
        this.weapon.Hit(target);
    }
}

如果我們需要用其他武器就不需要修改Samurai了。我們再建立另外一種武器。

class Shuriken : IWeapon
{
    public void Hit(string target)
    {
        Console.WriteLine("Pierced {0}'s armor", target);
    }
}

現在我們可以建立裝備不同武器的戰士了

class Program
{
    public static void Main()
    {
        var warrior1 = new Samurai(new Shuriken());
        var warrior2 = new Samurai(new Sword());
        warrior1.Attack("the evildoers");
        warrior2.Attack("the evildoers");
    }
}

列印出如下結果:

Pierced the evildoers armor.
Chopped the evildoers clean in half.
至此已解決了依賴項問題,以上的做法我們稱為手工依賴注入。
每次需要建立一個 Samurai時都必須首先創造一個 IWeapon介面的實現,然後傳遞到 Samurai的建構函式中。
但如何對介面的具體實現進行例項化而無須在應用程式的某個地方建立依賴項呢? 按照現在的情況,在應用程式的某個地方仍然需要以下這些語句。

IWeapon weapon = new Sword();
var warrior = new Samurai(weapon);

這實際上是將依賴項往後移了,例項化時還是需要對Program中進行修改,這破壞了無須修改Program就能替換武器的目的。
我們需要達到的效果是,能夠獲取實現某介面的物件,而又不必直接建立該物件,即 自動依賴項注入。
解決辦法是使用Dependency Injection Container, DI容器。
以上面的例子來說,它在類(Program)所宣告的依賴項和用來解決這些依賴項的類(Sword)之間充當中介軟體的角色。
可以用DI容器註冊一組應用程式要使用的介面或抽象型別,並指明滿足依賴項所需例項化的實現類。因此在上例中,便會用DI容器註冊IWeapon介面,並指明在需要實現IWeapon時,應該建立一個Sword的例項。DI容器會將這兩項資訊結合在一起,從而建立Sword物件,然後用它作為建立Program的一個引數,於是在應用程式中便可以使用這個Sword了。
接下來,我們就演示下如何使用Ninject這個DI容器。

引入Ninject

為方便在MVC中測試,我們對前面的類稍作調整。
Models資料夾中分別建如下檔案:

namespace XEngine.Web.Models
{
    public interface IWeapon
    {
        string Hit(string target);
    }
}

namespace XEngine.Web.Models
{
    public class Sword:IWeapon
    {
        public string Hit(string target)
        {
            return string.Format("Chopped {0} clean in half", target);
        }
    }
}

namespace XEngine.Web.Models
{
    public class Shuriken:IWeapon
    {
        public string Hit(string target)
        {
            return string.Format("Pierced {0}'s armor", target);
        }
    }
}

namespace XEngine.Web.Models
{
    public class Samurai
    {
        readonly IWeapon weapon;
        public Samurai(IWeapon weapon)
        {
            this.weapon = weapon;
        }

        public string Attack(string target)
        {
            return this.weapon.Hit(target);
        }
    }
}

測試的HomeController.cs檔案裡增加一個Action

public ActionResult Battle()
{
    var warrior1 = new Samurai(new Sword());
    ViewBag.Res = warrior1.Attack("the evildoers");
    return View();
}

最後是Action對應的View

@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Battle</title>
</head>
<body>
    <div> 
        @ViewBag.Res
    </div>
</body>
</html>

執行將會看到字串:Chopped the evildoers clean in half
好了,準備工作都已OK,下面我們就引入Ninject

一、將Ninject新增到專案中

在VS中選擇 Tools -> Library Package Manager -> Package Manager Console
輸入如下命令:

install-package ninject
install-package Ninject.Web.Common

執行結果如下:

PM> install-package ninject
正在安裝“Ninject 3.2.2.0”。
您正在從 Ninject Project Contributors 下載 Ninject,有關此程式包的許可協議在 https://github.com/ninject/ninject/raw/master/LICENSE.txt 上提供。請檢查此程式包是否有其他依賴項,這些依賴項可能帶有各自的許可協議。您若使用程式包及依賴項,即構成您接受其許可協議。如果您不接受這些許可協議,請從您的裝置中刪除相關元件。
已成功安裝“Ninject 3.2.2.0”。
正在將“Ninject 3.2.2.0”新增到 XEngine.Web。
已成功將“Ninject 3.2.2.0”新增到 XEngine.Web。

PM> install-package Ninject.Web.Common
正在嘗試解析依賴項“Ninject (≥ 3.2.0.0 && < 3.3.0.0)”。
正在安裝“Ninject.Web.Common 3.2.3.0”。
您正在從 Ninject Project Contributors 下載 Ninject.Web.Common,有關此程式包的許可協議在 https://github.com/ninject/ninject.extensions.wcf/raw/master/LICENSE.txt 上提供。請檢查此程式包是否有其他依賴項,這些依賴項可能帶有各自的許可協議。您若使用程式包及依賴項,即構成您接受其許可協議。如果您不接受這些許可協議,請從您的裝置中刪除相關元件。
已成功安裝“Ninject.Web.Common 3.2.3.0”。
正在將“Ninject.Web.Common 3.2.3.0”新增到 XEngine.Web。
已成功將“Ninject.Web.Common 3.2.3.0”新增到 XEngine.Web。

安裝完成後就可以使用了,我們修改下HomeController中的Action方法

二、使用Ninject完成繫結功能

基本的功能分三步:
建立核心,配置核心(指定介面和需要繫結類),建立具體物件
具體如下:

public ActionResult Battle()
{
    //var warrior1 = new Samurai(new Sword());
    //1. 建立一個Ninject的核心例項
    IKernel ninjectKernel = new StandardKernel();
    //2. 配置Ninject核心,指明介面需繫結的類
    ninjectKernel.Bind<IWeapon>().To<Sword>();
    //3. 根據上一步的配置建立一個物件
    var weapon=ninjectKernel.Get<IWeapon>();
    var warrior1 = new Samurai(weapon);

    ViewBag.Res = warrior1.Attack("the evildoers");
    return View();
}

檢視下View中的結果,和一開始一模一樣

介面具體需要例項化的類是通過Get來獲取的,根據字面意思,程式碼應該很容易理解,我就不多做解釋了。
我們完成了使用Ninject改造的第一步,不過目前介面和實現類繫結仍是在HomeController中定義的,下面我們再進行一輪重構,在HomeController中去掉這些配置。

第二輪重構

通過建立、註冊依賴項解析器達到自動依賴項注入。

一、建立依賴項解析器

這裡的依賴項解析器所做的工作就是之前Ninject基本功能的三個步驟: 建立核心,配置核心(指定介面和繫結類),建立具體物件。我們通過實現System.Mvc名稱空間下的IDependencyResolver介面來實現依賴項解析器。
待實現的介面:

namespace System.Web.Mvc
{
    // 摘要: 
    //     定義可簡化服務位置和依賴關係解析的方法。
    public interface IDependencyResolver
    {
        // 摘要: 
        //     解析支援任意物件建立的一次註冊的服務。
        //
        // 引數: 
        //   serviceType:
        //     所請求的服務或物件的型別。
        //
        // 返回結果: 
        //     請求的服務或物件。
        object GetService(Type serviceType);
        //
        // 摘要: 
        //     解析多次註冊的服務。
        //
        // 引數: 
        //   serviceType:
        //     所請求的服務的型別。
        //
        // 返回結果: 
        //     請求的服務。
        IEnumerable<object> GetServices(Type serviceType);
    }
}

具體實現:

namespace XEngine.Web.Infrastructure
{
    public class NinjectDependencyResolver:IDependencyResolver
    {
        private IKernel kernel;
        public NinjectDependencyResolver(IKernel kernelParam)
        {
            kernel = kernelParam;
            AddBindings();
        }
        public object GetService(Type serviceType)
        {
            return kernel.TryGet(serviceType);
        }
        public IEnumerable<object> GetServices(Type serviceType)
        {
            return kernel.GetAll(serviceType);
        }
        private void AddBindings()
        {
            kernel.Bind<IWeapon>().To<Sword>();
        }
    }
}

MVC框架在需要類例項以便對一個傳入的請求進行服務時,會呼叫GetService或GetServices方法。依賴項解析器要做的工作便是建立這一例項。

二、註冊依賴項解析器

還剩最後一步,註冊依賴項解析器。
再次開啟Package Manager Console
輸入如下命令:

install-package Ninject.MVC5

執行結果

PM> install-package Ninject.MVC5
正在嘗試解析依賴項“Ninject (≥ 3.2.0.0 && < 3.3.0.0)”。
正在嘗試解析依賴項“Ninject.Web.Common.WebHost (≥ 3.0.0.0)”。
正在嘗試解析依賴項“Ninject.Web.Common (≥ 3.2.0.0 && < 3.3.0.0)”。
正在嘗試解析依賴項“WebActivatorEx (≥ 2.0 && < 3.0)”。
正在嘗試解析依賴項“Microsoft.Web.Infrastructure (≥ 1.0.0.0)”。
正在安裝“WebActivatorEx 2.0”。
已成功安裝“WebActivatorEx 2.0”。
正在安裝“Ninject.Web.Common.WebHost 3.2.0.0”。
您正在從 Ninject Project Contributors 下載 Ninject.Web.Common.WebHost,有關此程式包的許可協議在 https://github.com/ninject/ninject.web.common/raw/master/LICENSE.txt 上提供。請檢查此程式包是否有其他依賴項,這些依賴項可能帶有各自的許可協議。您若使用程式包及依賴項,即構成您接受其許可協議。如果您不接受這些許可協議,請從您的裝置中刪除相關元件。
已成功安裝“Ninject.Web.Common.WebHost 3.2.0.0”。
正在安裝“Ninject.MVC5 3.2.1.0”。
您正在從 Remo Gloor,   Ian Davis 下載 Ninject.MVC5,有關此程式包的許可協議在 https://github.com/ninject/ninject.web.mvc/raw/master/mvc3/LICENSE.txt 上提供。請檢查此程式包是否有其他依賴項,這些依賴項可能帶有各自的許可協議。您若使用程式包及依賴項,即構成您接受其許可協議。如果您不接受這些許可協議,請從您的裝置中刪除相關元件。
已成功安裝“Ninject.MVC5 3.2.1.0”。
正在將“WebActivatorEx 2.0”新增到 XEngine.Web。
已成功將“WebActivatorEx 2.0”新增到 XEngine.Web。
正在將“Ninject.Web.Common.WebHost 3.2.0.0”新增到 XEngine.Web。
已成功將“Ninject.Web.Common.WebHost 3.2.0.0”新增到 XEngine.Web。
正在將“Ninject.MVC5 3.2.1.0”新增到 XEngine.Web。
已成功將“Ninject.MVC5 3.2.1.0”新增到 XEngine.Web。

可以看到App_Start資料夾下多了一個 NinjectWebCommon.cs檔案,它定義了應用程式啟動時會自動呼叫的一些方法,將它們整合到ASP.NET的請求生命週期之中。

MVC 5 + EF6 完整教程15 -- 使用DI進行解耦

找到最後一個方法RegisterServices,只需要新增一句即可。

public static class NinjectWebCommon 
{

    /// <summary>
    /// Load your modules or register your services here!
    /// </summary>
    /// <param name="kernel">The kernel.</param>
    private static void RegisterServices(IKernel kernel)
    {
        System.Web.Mvc.DependencyResolver.SetResolver(new XEngine.Web.Infrastructure.NinjectDependencyResolver(kernel));
    }        
}

三、重構HomeController

主要新增一個建構函式來接收介面的實現,如下

private IWeapon weapon;

public HomeController(IWeapon weaponParam)
{
    weapon = weaponParam;
}

public ActionResult Battle()
{

    //var warrior1 = new Samurai(new Sword());

    ////1. 建立一個Ninject的核心例項
    //IKernel ninjectKernel = new StandardKernel();
    ////2. 配置Ninject核心,指明介面需繫結的類
    //ninjectKernel.Bind<IWeapon>().To<Sword>();
    ////3. 根據上一步的配置建立一個物件
    //var weapon=ninjectKernel.Get<IWeapon>();

    var warrior1 = new Samurai(weapon);

    ViewBag.Res = warrior1.Attack("the evildoers");
    return View();
}

執行可以看到和之前一樣的效果。
這種依賴項是在執行中才被注入到HomeController中的,這就是說,在類的例項化期間才會建立IWeapon介面的實現類例項,並將其傳遞給HomeController構造器。HomeController與依賴項介面的實現類直接不存在編譯時的依賴項。
我們完全可以用另一個武器而無需對HomeController做任何修改。

總結

DI是一種實現元件解耦的設計模式。分成兩個步驟:

  1. 打斷和宣告依賴項
    建立一個類建構函式,以所需介面的實現作為其引數,去除對具體類的依賴項。
  2. 注射依賴項
    通過建立、註冊依賴項解析器達到自動依賴項注入。

依賴項注入除了通過建構函式的方式還可以通過屬性注入和方法注入,展開講還有很多東西,我們還是按照一貫的風格,夠用就好,先帶大家掃清障礙,大家先直接模仿著實現就好了。
進一步學習可以參考官網學習教程:https://github.com/ninject/Ninject/wiki
後續文章專案實戰部分,會根據專案實際需求,用到時再展開講。

祝學習進步:)

相關文章