6.MVC工具之Ninject

  • 时间:2019-05-31
  • 作者:Charles
  • 热度:1709

接下来的三篇文章将介绍三个工具:DI容器、单元测试框架和模仿工具。并演示它们的核心特性。

一:创建实例项目

创建一个名为EssentialTools的MVC空项目,接下来的三篇文章将用此项目演示。

1.1   创建模型类

在项目的Models文件夹下添加一个名为Product的类文件和一个名为LinqValueCalculator类文件,LinqValueCalculator用于计算Product对象集合的总价,如下图所示。

LinqValueCalculator类定义了一个单一的方法ValueProducts,它使用LINQ的Sum方法将传递给该方法的可枚举对象中每一个Product对象的Price属性值加和在一起,这是经常使用的一个很好的LINQ特性。

再创建一个名为ShoppingCart的新类,用它表示Product对象的集合,并且使用LinqValueCalculator来确定总价,如下图所示。

1.2   添加控制器

在Controllers文件夹下添加一个Home控制器,并创建一个product对象数组,使用LinqValueCalculator类计算出总价的值,将其传递给View方法。如下图所示。

1.3   添加视图

添加一个名为Index的视图,这是一个以decimal为视图模型对象的强类型视图,修改其内容,如下图所示。

启动项目,将会在浏览器看到如下页面。

二:使用Ninject

2.1 理解问题

在该示例中,创造了一个能够用DI解决的问题。该项目依赖于一些紧耦合的类:ShoppingCart类与LinqValueCalculator类是紧耦合的,二HomeController类与ShoppingCart类和LinqValueCalculator类都是紧耦合关系。这意味着,如果想替换LinqValueCalculator类,就必须在与它有紧耦合关系的所有类中找出对它的引用,并进行修改。这在一个中大型项目中,将是一个繁琐而易出错的过程。

运用接口

通过使用C#接口,从计算器的实现中国抽象出其功能定义,可以解决部分问题,在Models文件夹下新增IValueCalculator接口类文件,如下图所示。

然后,在LinqValueCalculator类中实现该接口,如下图所示。

该接口能够打断ShoppingCart与LinqValueCalculator类之间的紧耦合关系,修改ShoppingCart类文件,如下图所示。

虽然已经取得了一些进展,但是C#要求接口实例化时需要指定其实现类。这很自然,因为它需要知道用户想用的是哪一个实现类。这意味着,Home控制器在创建LinqValueCalculator对象时仍有问题。如下图所示。

使用Ninject的目的就是要解决这一问题,用以对IValueCalculator接口的实现进行实例化,但所需的实现细节却又不是Home控制器代码的一部分,即,通过Ninject可以去掉控制器中选中部分的代码,这项工作由Ninject完成,这样便去掉了Home控制器与总价计算器LinqValueCalculator之间的耦合。

2.2 将Ninject添加到项目

对MVC项目添加Ninject最简单的方法是使用Visual Studio对NuGet的集成支持,这使vs易于安装各种各样的包,并保持这个包为最新。选择“工具—NuGet包管理器—管理解决方案的NuGet程序包”,打开NuGet界面,点击左上角浏览选项卡,在搜索框输入Ninject,将会看到一系列的Ninject包。

点击Ninject库的下箭头按钮,确认下载对话框后,vs将完成该库的下载并将其安装到项目中。

当然,也可以直接去官网下载该库最新版,并手动添加引用,在这里不再赘述。

2.3 Ninject初步

为了得到Ninject的基本功能,要做的工作由三个步骤,修改Home控制器,如下图所示。

第一个步骤是准备使用Ninject,这需要创建一个Ninject内核的实例,这是一个对象,本例将用它与Ninject进行通信,并请求接口的实现。本例创建内核语句如下。

IKernel ninjectKernel = new StandardKernel();

需要创建一个Ninject.IKernel接口的实现,此事可通过创建一个StandardKernel类的新实例来完成。这样能够执行第二个步骤,即建立应用程序中的接口和想要使用的实现类之间的关系。以下为示例中完成这一工作的语句。

ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();

Ninject使用C#的类型参数创建了一种关系:将想要使用的接口设置为Bind方法的类型参数,并在其返回的结果上调用To方法,将希望实例化的实例类设置为To方法的类型参数。该语句告诉Ninject:当要求它实现IValueCalculator接口时,应当创建LinqValueCalculator类的新实例,以便对该请求进行服务。

最后一个步骤是实际使用Ninject,通过Get方法完成这一工作,如下所示。

IValueCalculator calc = ninjectKernel.Get<IValueCalculator>();

Get方法所用的类型参数告诉Ninject,用户感兴趣的是哪一个接口,而该方法的结果是刚才用To方法指定的实现类型的一个实例。

2.4 建立MVC依赖性注入

 前述三个步骤的结果是:在Ninject中已经建立了一些相关知识,即使用哪一个实现类来完成对IValueCalculator接口的请求。但是应用程序未作任何改进,因为这种只是还只是在Home控制器中定义的,即Home控制器与LinqValueCalculator类仍然紧紧耦合的。

创建依赖解析器

首先建立一个自定义的依赖解析器。MVC框架需要使用依赖解析器来创建类的实例,以便对请求进行服务。通过创建自定义解析器,保证每当要创建一个对象时,便使用Ninject。

在项目中添加一个名为Infrastructure的文件夹,并添加一个名为NinjectDependencyResolver.cs的新类。如下图所示。

MVC框架在需要一个类实例以便对一个传入请求进行服务时,会调用GetService或GetServices方法。依赖解析器要做的便是创建这一实例,这个任务由Ninject的TryGet和GetAll方法来完成。TryGet的工作方式类似于前面所用的Get方法,但当没有合适的绑定时,它会返回一个null,而不是抛出一个异常。GetAll方法支持对单一类型的多个绑定,当有多个不同的服务提供器时可以使用它。

注册依赖解析器

必须告诉MVC框架,用户希望使用自己的依赖解析器,此事可通过修改Global.asax.cs文件来完成。如下图所示。

重构Home控制器

重构Home控制器,以使它能够利用前面所建立的工具。如下图所示。

所做的主要修改时添加了一个构造器,它接收IValueCalculator接口的实现。实例并未指定想要使用哪一个实现,而且已经添加了一个名为calc的实例变量,可以在整个控制器类中用它来表示构造器所接收到的IValueCalculator。

所作的另一个修改时删除了任何关于Ninject或LinqValurCalculator类的代码,最终打破了Home控制器与LinqValurCalculator类之间的耦合。运行示例,将会看到如下图所示输出。

示例创建的时一种构造器注入实例,这是依赖性注入的一种形式。以下为运行应用程序,浏览器请求应用程序根URL时所发生的情况。

1.     浏览器向MVC框架发送一个请求Home的URL,MVC框架猜出该请求意指Home控制器,于是会创建HomeController类实例。

2.     在创建HomeController类实例过程中会发现其构造器有一个对IValueCalculator接口的依赖项,于是会要求依赖性解析器对此依赖项进行解析,将接口指定为依赖性解析器中GetService方法所使用的类型参数。

3.     依赖性解析器会将传递过来的类型参数交给TryGet方法,要求Ninject创建一个新的IValueCalculator接口类实例。

4.     Ninject会检测到该接口与其实现类LinqValueCalculator具有绑定关系,于是为该接口创建一个LinqValueCalculator类实例,并将其回递给依赖性解析器。

5.     依赖性解析器将Ninject所返回的LinqValueCalculator类作为IValueCalculator接口实现类实例回递给MVC框架。

6.     MVC框架利用依赖性解析器返回的接口类实例创建HomeController控制器实例,并使用该控制器实例对请求进行服务。

这里所采取的办法其好处之一是,任何控制器都可以在其构造器中声明一个IValueCalculator,并通过自定义依赖性解析器使用Ninject来创建一个在AddBinding方法中指定的实现实例。

所得到的最大好处是,在希望用另一个实现来替代LinqValueCalculator时,只需要对依赖性解析器类进行修改,因为为了满足对于IValueCalculator接口的请,这里是唯一一处处理该接口指定实现类的地方。

创建依赖性链

当要求Ninject创建一个类型时,它会检查该类型与其他类型之间的耦合。如果由额外的依赖性,Ninject会自动地解析这些依赖性,并创建所需要的所有类的实例。在Models文件夹下添加一个名为Discount的文件,并定义一个新的接口及其实现类,如下图所示。

 修改LinqValueCalculator类,以使它执行计算时使用IDiscountHelper接口,如下图所示。

这个类新添加的构造器以IDiscountHelper的接口实现为参数,然后将其用于ValueProducts方法以便于对所处理的Product对象的累计值运用一个折扣。

在NinjectDependencyResolver类中,用Ninject内核将IDiscountHelper接口与其实现类进行绑定,如下图所示。

上述做法已经创建了一个Ninject可以轻松创建的依赖性链,这是通过在自定义依赖性解析器中所定义的绑定实现的。为了满足对HomeController类的请求,Ninject会意识到它需要创建一个用于IValueCalculator类的实现,通过考察其绑定,会看到该接口的实现策略是使用LinqValueCalculator类。但在创建LinqValueCalculator对象过程中,Ninject又会意识到它需要使用IDiscountHelper接口实现,因此会查看其绑定,并创建一个DefaultDiscountHelper对象。Ninject将DefaultDiscountHelper对象传递给LinqValueCalculator对象的构造器,LinqValueCalculator对象而又被转给HomeController类的构造器,最后所得到的HomeController用于对用户的请求进行服务。Ninject会以这种方式检查它要实例化的每一个依赖性类,无论其依赖性链有多长多复杂。

2.5 指定属性与构造器参数

把接口绑定到它的实现时,可以提供想要运用到属性上的一些属性细节,以便对Ninject创建的类进行配置。修改DefaultDiscountHelper类,如下图所示。

在用Ninject将具体类绑定时,读者可以使用WithPropertyValue方法来设置DefaultDiscountHelper类中的DiscountSize属性的值。如下图所示。

如果需要设置多个属性值,可以链接调用WithPropertyValue方法涵盖所有这些属性,也可以使用构造器参数做同样的事情。重写DefaultDiscount类,以折扣大小作为构造器参数进行传递,如下图所示。

在AddBindings方法中用WithConstructorArgument方法来指定构造器参数的值,如下图所示。

这一技术允许用户将一个值注入到构造器中,同样,可以将这些方法调用链接在一起,以提供多值,并与依赖性混合匹配。Ninject会判断出用户的需要,并以此来创建它。运行应用程序,将会得到相同输出,如下图所示。

2.6 使用条件绑定

Ninject支持多个条件的绑定方法,者能够指定用哪一个类对某一个特定的请求进行响应。在Models文件夹下新建一个名为FlexibleDiscountHelper新文件,用于演示此特性,内容如下图所示。

FlexibleDiscountHelper类根据要打折的总额大小运用不同的折扣,修改AddBindings方法,以告诉Ninject何时使用FlexibleDiscountHelper,何时使用DefaultDiscountHelper。如下图所示。

此绑定说明,在Ninject要将一个实现注入LinqValueCalculator对象时,应该使用FlexibleDiscountHelper类作为IDiscountHelper接口的实现。



博主声明

1、本博客主要为原创文章,转载请注明出处。

2、部分文章来自网络,已注明出处,如有侵权请与本人联系。

3、如果文章内容有误,或者您有其他更好的意见、建议请给我留言,我会及时处理!