MVC 5 + EF6 完整教程15 -- 使用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是一种实现组件解耦的设计模式。分成两个步骤:
- 打断和声明依赖项
创建一个类构造函数,以所需接口的实现作为其参数,去除对具体类的依赖项。 - 注射依赖项
通过创建、注册依赖项解析器达到自动依赖项注入。
依赖项注入除了通过构造函数的方式还可以通过属性注入和方法注入,展开讲还有很多东西,我们还是按照一贯的风格,够用就好,先带大家扫清障碍,大家先直接模仿着实现就好了。