如果大家研究一些開源項目,會發現無處不在的DI(Dependency Injection依賴註入)。 本篇文章將會詳細講述如何在MVC中使用Ninject實現DI 文章提綱 場景描述 & 問題引出 第一輪重構 引入Ninject 第二輪重構 總結 場景描述 & 問題引出 DI是一種實現組件解耦的設計 ...
如果大家研究一些開源項目,會發現無處不在的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);
}
修改 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的請求生命周期之中。
找到最後一個方法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是一種實現組件解耦的設計模式。分成兩個步驟:
- 打斷和聲明依賴項
創建一個類構造函數,以所需介面的實現作為其參數,去除對具體類的依賴項。 - 註射依賴項
通過創建、註冊依賴項解析器達到自動依賴項註入。
依賴項註入除了通過構造函數的方式還可以通過屬性註入和方法註入,展開講還有很多東西,我們還是按照一貫的風格,夠用就好,先帶大家掃清障礙,大家先直接模仿著實現就好了。
進一步學習可以參考官網學習教程:https://github.com/ninject/Ninject/wiki
後續文章項目實戰部分,會根據項目實際需求,用到時再展開講。
祝學習進步:)