如果大家研究一些开源项目,会发现无处不在的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是一种实现组件解耦的设计模式。分成两个步骤:

  1. 打断和声明依赖项

    创建一个类构造函数,以所需接口的实现作为其参数,去除对具体类的依赖项。
  2. 注射依赖项

    通过创建、注册依赖项解析器达到自动依赖项注入。

依赖项注入除了通过构造函数的方式还可以通过属性注入和方法注入,展开讲还有很多东西,我们还是按照一贯的风格,够用就好,先带大家扫清障碍,大家先直接模仿着实现就好了。

进一步学习可以参考官网学习教程:https://github.com/ninject/Ninject/wiki

后续文章项目实战部分,会根据项目实际需求,用到时再展开讲。

祝学习进步:)

MVC 5 + EF6 完整教程15 -- 使用DI进行解耦的更多相关文章

  1. MVC 5 + EF6 完整教程16 -- 控制器详解

    Controller作为持久层和展现层的桥梁, 封装了应用程序的逻辑,是MVC中的核心组件之一. 本篇文章我们就来谈谈 Controller, 主要讨论两个方面: Controller运行机制简介 C ...

  2. MVC5+EF6 完整教程

    随笔分类 - MVC ASP.NET MVC MVC5+EF6 完整教程17--升级到EFCore2.0 摘要: EF Core 2.0上周已经发布了,我们也升级到core 文章内容基于vs2017, ...

  3. 开源题材征集 + MVC&EF Core 完整教程小结

    到目前为止,我们的MVC+EF Core 完整教程的理论部分就全部结束了,共20篇,覆盖了核心的主要知识点. 下一阶段是实战部分,我们将会把这些知识点串联起来,用10篇(天)来完成一个开源项目. 现向 ...

  4. MVC5+EF6 完整教程17--升级到EFCore2.0(转)

    MVC5+EF6 完整教程17--升级到EFCore2.0 2017年08月22日 14:48:12 linux12a 阅读数:2814   EF Core 2.0上周已经发布了,我们也升级到core ...

  5. MVC5+EF6 完整教程17--升级到EFCore2.0

    EF Core 2.0上周已经发布了,我们也升级到core 文章内容基于vs2017,请大家先安装好vs2017(15.3). 本篇文章主要讲下差异点,跟之前一样的就不再重复了. 文章目录(差异点): ...

  6. MVC5 + EF6 完整教程 (转)

    点击查看: MVC5 + EF6

  7. MVC+EF Core 完整教程20--tag helper详解

    之前我们有一篇:“动态生成多级菜单”,对使用Html Helper做了详细讲述,并且自定义了一个菜单的 Html Helper: https://www.cnblogs.com/miro/p/5541 ...

  8. MVC5 + EF6 入门完整教程1

    https://www.cnblogs.com/miro/p/4030622.html 第0课 从0开始 ASP.NET MVC开发模式和传统的WebForm开发模式相比,增加了很多"约定& ...

  9. MVC5+EF6 入门完整教程11--细说MVC中仓储模式的应用

    摘要: 第一阶段1~10篇已经覆盖了MVC开发必要的基本知识. 第二阶段11-20篇将会侧重于专题的讲解,一篇文章解决一个实际问题. 根据园友的反馈, 本篇文章将会先对呼声最高的仓储模式进行讲解. 文 ...

随机推荐

  1. 学习笔记——Java类和对象

    今天学习了Java的类和对象的相关知识,由于Java面向对象的编程的思想和C++几乎一样,所以需要更多的关注Java的一些不同之处. 1.类 1.1 在类这一块,除了基本的成员变量,成员方法,构造函数 ...

  2. 阅读Facebook POP框架 笔记(一)

    在这一系列文章里,我主要会将自己阅读第三方代码的经历记录下来,尝试独立分析解剖一个框架.之前也阅读过一些第三方代码,但是实际上来说对自己的成长并没有太大的帮助,因为阅读的不细致,没有领会到代码的精髓. ...

  3. Docker存储驱动之Btrfs简介

    简介 Btrfs是下一代的copy-on-write文件系统,它支持很多高级特性,使其更加适合Docker.Btrfs合并在内核主线中,并且它的on-disk-format也逐渐稳定了.不过,它的很多 ...

  4. 第六讲:CPU虚拟化

    虚拟化技术的分类主要有服务器虚拟化.存储虚拟化.网络虚拟化.应用虚拟化. 服务器虚拟化技术按照虚拟对象来分,可分为:CPU虚拟化.内存虚拟化.I/O虚拟化: 按照虚拟化程度可分为:全虚拟化.半虚拟化. ...

  5. 每天一个linux命令(24)--Linux文件类型与扩展名

    linux 文件类型和Linux 文件的文件名所代表的意义是两个不同的概念.我们通过一般应用程序而创建的比如 file.txt  file.tar.gz.这些文件虽然要用不同的程序来打开,但放在Lin ...

  6. C++ 11和C++98相比有哪些新特性

    此文是如下博文的翻译: https://herbsutter.com/elements-of-modern-c-style/ C++11标准提供了许多有用的新特性.这篇文章特别针对使C++11和C++ ...

  7. Oracle Developer Data Modeler项目实践 (转)

    http://www.Oracle.com/webfolder/technetwork/tutorials/obe/db/sqldevdm/r30/datamodel2moddm/datamodel2 ...

  8. [vijosP1303]导弹拦截(最长上升子序列转LCS)

    描述 某国为了防御敌国的导弹袭击,研发出一种导弹拦截系统.但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度.某天,雷达捕捉到敌国的导弹来袭 ...

  9. Digital Tutors - Creating an Action Adventure Puzzle in Unity学习笔记

    遇到的问题: 1 第11节Scripting the pressure plates中需要获取子物体的Animator组件,教程使用的语句如下: ”SwitchAnim = GetComponentI ...

  10. AJAX遮罩实例

    function transferip() { var site_list=$("textarea[name='Oldsite']").val(); var ip_list=$(& ...