Home >Backend Development >C#.Net Tutorial >[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

黄舟
黄舟Original
2016-12-30 14:03:591293browse

[ASP.NET
MVC Mavericks Road]04 - Dependency Injection (DI) and Ninject

Directory of this article:


Why Dependency injection is required

In the [ASP.NET MVC Maverick's Road] series of articles on understanding the MVC pattern, we mentioned that an important feature of MVC is the separation of concerns. We want the various components of the application to be as independent as possible and have as few dependencies on each other as possible.

Our ideal situation is: a component does not know or care about other components, but it can realize the function calls of other components through the provided public interface. This situation is called loose coupling.

Give a simple example. We want to customize an "advanced" price calculator LinqValueCalculator for the product. This calculator needs to implement the IValueCalculator interface. The following code is shown:

public interface IValueCalculator {
    decimal ValueProducts(params Product[] products);
}

public class LinqValueCalculator : IValueCalculator {
    public decimal ValueProducts(params Product[] products) {
        return products.Sum(p => p.Price);
    }
}

The Product class is the same as that used in the first two blog posts. Now there is a shopping cart ShoppingCart class, which needs to have a function that can calculate the total price of the items in the shopping cart. However, the shopping cart itself does not have a calculation function. Therefore, the shopping cart must embed a calculator component. This calculator component can be a LinqValueCalculator component, but not necessarily a LinqValueCalculator component (in the future, when the shopping cart is upgraded, other more advanced calculations may be embedded. device). Then we can define the shopping cart ShoppingCart class like this:

1 public class ShoppingCart {
 2     //计算购物车内商品总价钱
 3     public decimal CalculateStockValue() {
 4         Product[] products = { 
 5             new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, 
 6             new Product {Name = "苹果", Category = "水果", Price = 4.9M}, 
 7             new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, 
 8             new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} 
 9         };
10         IValueCalculator calculator = new LinqValueCalculator();
11 
12         //计算商品总价钱 
13         decimal totalValue = calculator.ValueProducts(products);
14 
15         return totalValue;
16     }
17 }


The ShoppingCart class calculates the total price of the product through the IValueCalculator interface (not through LinqValueCalculator). If you need to use a more advanced calculator when upgrading your shopping cart in the future, you only need to change the object after new in the 10th line of code (that is, replace the LinqValueCalculator), and the other code does not need to be changed. This achieves a certain degree of loose coupling. At this time, the relationship between the three is as shown in the following figure:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

This figure shows that the ShoppingCart class depends on both the IValueCalculator interface and the LinqValueCalculator class. There is a problem with this. In real-world terms, if the calculator component embedded in the shopping cart breaks, it will cause the entire shopping cart to not work properly. Wouldn't it be necessary to replace the entire shopping cart! The best way is to completely separate the calculator component and the shopping cart, so that no matter which component is broken, you only need to replace the corresponding component. That is, the problem we want to solve is to completely disconnect the ShoppingCart component and the LinqValueCalculator component, and the design pattern of dependency injection is to solve this problem.

What is Dependency Injection

The partial loose coupling achieved above is obviously not what we need. What we need is, within a class, to be able to obtain a reference to an object that implements a public interface without creating an instance of the object. This "need" is called DI (Dependency Injection), which has the same meaning as the so-called IoC (Inversion of Control).

DI is a design pattern that achieves loose coupling through interfaces. Beginners may wonder why there are so many technical articles on the Internet that focus on DI. It is because DI is an important concept that developers must have to develop applications efficiently under almost all frameworks. Includes MVC development. It is an important means of decoupling.

DI mode can be divided into two parts. One is to remove the dependence on the component (LinqValueCalculator in the above example), and the other is to pass a reference to the component that implements the public interface through the class's constructor (or the class's Setter accessor). As shown in the following code:

public class ShoppingCart {
    IValueCalculator calculator;
    
    //构造函数,参数为实现了IValueCalculator接口的类的实例
    public ShoppingCart(IValueCalculator calcParam) {
        calculator = calcParam;
    }

    //计算购物车内商品总价钱
    public decimal CalculateStockValue() {
        Product[] products = { 
            new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, 
            new Product {Name = "苹果", Category = "水果", Price = 4.9M}, 
            new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, 
            new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} 
        };

        //计算商品总价钱 
        decimal totalValue = calculator.ValueProducts(products);

        return totalValue;
    }
}

In this way, we completely disconnect the dependency between ShoppingCart and LinqValueCalculator. An instance reference of a class that implements the IValueCalculator interface (LinqValueCalculator in the example) is passed as a parameter to the constructor of the ShoppingCart class. But the ShoppingCart class does not know or care about the class that implements the IValueCalculator interface, and it has no responsibility to operate this class. At this time we can use the following figure to describe the relationship between ShoppingCart, LinqValueCalculator and IValueCalculator:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

在程序运行的时候,依赖被注入到ShoppingCart,这个依赖就是,通过ShoppingCart构造函数传递实现了IValueCalculator接口的类的实例引用。在程序运行之前(或编译时),ShoppingCart和任何实现IValueCalculator接口的类没有任何依赖关系。(注意,程序运行时是有具体依赖关系的。)

注意,上面示例使用的注入方式称为“构造注入”,我们也可以通过属性来实现注入,这种注入被称为“setter 注入”,就不举例了,朋友们可以看看T2噬菌体的文章依赖注入那些事儿来对DI进行更多的了解。

由于经常会在编程时使用到DI,所以出现了一些DI的辅助工具(或叫DI容器),如Unity和Ninject等。由于Ninject的轻量和使用简单,加上本人只用过Ninject,所以本系列文章选择用它来开发MVC应用程序。下面开始介绍Ninject,但在这之前,先来介绍一个安装Ninject需要用到的插件-NuGet。


使用NuGet安装库

NuGet 是一种 Visual Studio 扩展,它能够简化在 Visual Studio 项目中添加、更新和删除库(部署为程序包)的操作。比如你要在项目中使用Log4Net这个库,如果没有NuGet这个扩展,你可能要先到网上搜索Log4Net,再将程序包的内容解压缩到解决方案中的特定位置,然后在各项目工程中依次添加程序集引用,最后还要使用正确的设置更新 web.config。而NuGet可以简化这一切操作。例如我们在讲依赖注入的项目中,若要使用一个NuGet库,可直接右击项目(或引用),选择“管理NuGet程序包”(VS2010下为“Add
Library Package Reference”),如下图:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

在弹出如下窗口中选择“联机”,搜索“Ninject”,然后进行相应的操作即可:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

在本文中我们只需要知道如何使用NuGet来安装库就可以了。NuGet的详细使用方法可查看MSDN文档:使用 NuGet 管理项目库。


使用Ninject的一般步骤

在使用Ninject前先要创建一个Ninject内核对象,代码如下:

class Program { 
    static void Main(string[] args) { 
        //创建Ninject内核实例
        IKernel ninjectKernel = new StandardKernel(); 
    } 
}

使用Ninject内核对象一般可分为两个步骤。第一步是把一个接口(IValueCalculator)绑定到一个实现该接口的类(LinqValueCalculator),如下:

...
//绑定接口到实现了该接口的类
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator<(); 
...

这个绑定操作就是告诉Ninject,当接收到一个请求IValueCalculator接口的实现时,就返回一个LinqValueCalculator类的实例。

第二步是用Ninject的Get方法去获取IValueCalculator接口的实现。这一步,Ninject将自动为我们创建LinqValueCalculator类的实例,并返回该实例的引用。然后我们可以把这个引用通过构造函数注入到ShoppingCart类。如下代码所示:

...
// 获得实现接口的对象实例
IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>(); 
// 创建ShoppingCart实例并注入依赖
ShoppingCart cart = new ShoppingCart(calcImpl); 
// 计算商品总价钱并输出结果
Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
...

Ninject的使用的一般步骤就是这样。该示例可正确输出如下结果:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

但看上去Ninject的使用好像使得编码变得更加烦琐,朋友们会问,直接使用下面的代码不是更简单吗:

...
IValueCalculator calcImpl = new LinqValueCalculator();
ShoppingCart cart = new ShoppingCart(calcImpl);
Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
...

的确,对于单个简单的DI,用Ninject确实显得麻烦。但如果添加多个复杂点的依赖关系,使用Ninject则可大大提高编码的工作效率。


Ninject如何提高编码效率

当我们请求Ninject创建某个类型的实例时,它会检查这个类型和其它类型之间的耦合关系。如果存在依赖关系,那么Ninject会根据依赖处理理它们,并创建所有所需类的实例。为了解释这句话和说明使用Ninject编码的便捷,我们再创建一个接口IDiscountHelper和一个实现该接口的类DefaultDiscountHelper,代码如下:

//折扣计算接口
public interface IDiscountHelper {
    decimal ApplyDiscount(decimal totalParam);
}

//默认折扣计算器
public class DefaultDiscountHelper : IDiscountHelper {
    public decimal ApplyDiscount(decimal totalParam) {
        return (totalParam - (1m / 10m * totalParam));
    }
}

IDiscounHelper接口声明了ApplyDiscount方法,DefaultDiscounterHelper实现了该接口,并定义了打9折的ApplyDiscount方法。然后我们可以把IDiscounHelper接口作为依赖添加到LinqValueCalculator类中。代码如下:

public class LinqValueCalculator : IValueCalculator { 
    private IDiscountHelper discounter; 
 
    public LinqValueCalculator(IDiscountHelper discountParam) { 
        discounter = discountParam; 
    } 
 
    public decimal ValueProducts(params Product[] products) { 
        return discounter.ApplyDiscount(products.Sum(p => p.Price)); 
    } 
}

LinqValueCalculator类添加了一个用于接收IDiscountHelper接口的实现的构造函数,然后在ValueProducts方法中调用该接口的ApplyDiscount方法对计算出的商品总价钱进行打折处理,并返回折后总价。

到这,我们先来画个图理一理ShoppingCart、LinqValueCalculator、IValueCalculator以及新添加的IDiscountHelper和DefaultDiscounterHelper之间的关系:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

以此,我们还可以添加更多的接口和实现接口的类,接口和类越来越多时,它们的关系图看上去会像一个依赖“链”,和生物学中的分子结构图差不多。

按照前面说的使用Ninject的“二个步骤”,现在我们在Main中的方法中编写用于计算购物车中商品折后总价钱的代码,如下所示:

1 class Program {
 2     static void Main(string[] args) {
 3         IKernel ninjectKernel = new StandardKernel();
 4 
 5         ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
 6         ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>();
 7 
 8         IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();
 9         ShoppingCart cart = new ShoppingCart(calcImpl);
10         Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
11         Console.ReadKey();
12     }
13 }

输出结果:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

代码一目了然,虽然新添加了一个接口和一个类,但Main方法中只增加了第6行一句代码,获取实现IValueCalculator接口的对象实例的代码不需要做任何改变。

定位到代码的第8行,这一行代码,Ninject为我们做的事是:

  当我们需要使用IValueCalculator接口的实现时(通过Get方法),它便为我们创建LinqValueCalculator类的实例。而当创建LinqValueCalculator类的实例时,它检查到这个类依赖IDiscountHelper接口。于是它又创建一个实现了该接口的DefaultDiscounterHelper类的实例,并通过构造函数把该实例注入到LinqValueCalculator类。然后返回LinqValueCalculator类的一个实例,并赋值给IValueCalculator接口的对象(第8行的calcImpl)。

总之,不管依赖“链”有多长有多复杂,Ninject都会按照上面这种方式检查依赖“链”上的每个接口和实现接口的类,并自动创建所需要的类的实例。在依赖“链”越长越复杂的时候,更能显示使用Ninject编码的高效率。

Ninject的绑定方式

我个人将Ninject的绑定方式分为:一般绑定、指定值绑定、自我绑定、派生类绑定和条件绑定。这样分类有点牵强,只是为了本文的写作需要和方便读者阅读而分,[b]并不是官方的分类。[/b]

1、一般绑定

在前文的示例中用Bind和To方法把一个接口绑定到实现该接口的类,这属于一般的绑定。通过前文的示例相信大家已经掌握了,在这就不再累述。

2、[b]指定值绑定[/b]

我们知道,通过Get方法,Ninject会自动帮我们创建我们所需要的类的实例。但有的类在创建实例时需要给它的属性赋值,如下面我们改造了一下的DefaultDiscountHelper类:

public class DefaultDiscountHelper : IDiscountHelper { 
    public decimal DiscountSize { get; set; } 
 
    public decimal ApplyDiscount(decimal totalParam) { 
        return (totalParam - (DiscountSize / 10m * totalParam)); 
    } 
}


给DefaultDiscountHelper类添加了一个DiscountSize属性,实例化时需要指定折扣值(DiscountSize属性值),不然ApplyDiscount方法就没意义。而实例化的动作是Ninject自动完成的,怎么告诉Ninject在实例化类的时候给某属性赋一个指定的值呢?这时就需要用到参数绑定,我们在绑定的时候可以通过给WithPropertyValue方法传参的方式指定DiscountSize属性的值,如下代码所示:

public static void Main() {
    IKernel ninjectKernel = new StandardKernel();

    ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    ninjectKernel.Bind<IDiscountHelper>()
        .To<DefaultDiscountHelper>().WithPropertyValue("DiscountSize", 5M);

    IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();
    ShoppingCart cart = new ShoppingCart(calcImpl);
    Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
    Console.ReadKey();
}

只是在Bind和To方法后添加了一个WithPropertyValue方法,其他代码都不用变,再一次见证了用Ninject编码的高效。

WithPropertyValue方法接收了两个参数,一个是属性名(示例中的"DiscountSize"),一个是属性值(示例中的5)。运行结果如下:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

如果要给多个属性赋值,则可以在Bind和To方式后添加多个WithPropertyValue(,)方法。

我们还可以在类的实例化的时候为类的构造函数传递参数。为了演示,我们再把DefaultDiscountHelper类改一下:

public class DefaultDiscountHelper : IDiscountHelper { 
    private decimal discountRate; 
 
    public DefaultDiscountHelper(decimal discountParam) { 
        discountRate = discountParam; 
    } 
 
    public decimal ApplyDiscount(decimal totalParam) { 
        return (totalParam - (discountRate/ 10m * totalParam)); 
    } 
}

显然,DefaultDiscountHelper类在实例化的时候必须给构造函数传递一个参数,不然程序会出错。和给属性赋值类似,只是用的方法是WithConstructorArgument(,),绑定方式如下代码所示:

...
ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>(); 
ninjectKernel.Bind<IDiscountHelper>() 
    .To< DefaultDiscountHelper>().WithConstructorArgument("discountParam", 5M);
...

同样,只需要更改一行代码,其他代码原来怎么写还是怎么写。如果构造函数有多个参数,则需在Bind和To方法后面加上多个WithConstructorArgument即可。

3.自我绑定

Niject的一个非常好用的特性就是自绑定。当通过Bind和To方法绑定好接口和类后,可以直接通过ninjectKernel.Get()来获得一个类的实例。

在前面的几个示例中,我们都是像下面这样来创建ShoppingCart类实例的:

...
IValueCalculator calcImpl = ninjectKernel.Get<IValueCalculator>();
ShoppingCart cart = new ShoppingCart(calcImpl);
...

其实有一种更简单的定法,如下:

... 
ShoppingCart cart = ninjectKernel.Get<ShoppingCart>(); 
...

这种写法不需要关心ShoppingCart类依赖哪个接口,也不需要手动去获取该接口的实现(calcImpl)。当通过这句代码请求一个ShoppingCart类的实例的时候,Ninject会自动判断依赖关系,并为我们创建所需接口对应的实现。这种方式看起来有点怪,其实中规中矩的写法是:

...
ninjectKernel.Bind<ShoppingCart>().ToSelf();
ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();
...

这里有自我绑定用的是ToSelf方法,在本示例中可以省略该句。但用ToSelf方法自我绑定的好处是可以在其后面用WithXXX方法指定构造函数参数、属性等等的值。

4.派生类绑定

通过一般绑定,当请求一个接口的实现时,Ninject会帮我们自动创建实现接口的类的实例。我们说某某类实现某某接口,也可以说某某类继承某某接口。如果我们把接口当作一个父类,是不是也可以把父类绑定到一个继承自该父类的子类呢?我们来实验一把。先改造一下ShoppingCart类,给它的CalculateStockValue方法改成虚方法:

public class ShoppingCart {
    protected IValueCalculator calculator;
    protected Product[] products;

    //构造函数,参数为实现了IEmailSender接口的类的实例
    public ShoppingCart(IValueCalculator calcParam) {
        calculator = calcParam;
        products = new[]{ 
            new Product {Name = "西瓜", Category = "水果", Price = 2.3M}, 
            new Product {Name = "苹果", Category = "水果", Price = 4.9M}, 
            new Product {Name = "空心菜", Category = "蔬菜", Price = 2.2M}, 
            new Product {Name = "地瓜", Category = "蔬菜", Price = 1.9M} 
        };
    }

    //计算购物车内商品总价钱
    public virtual decimal CalculateStockValue() {
        //计算商品总价钱 
        decimal totalValue = calculator.ValueProducts(products);
        return totalValue;
    }
}

再添加一个ShoppingCart类的子类:

public class LimitShoppingCart : ShoppingCart {
    public LimitShoppingCart(IValueCalculator calcParam)
        : base(calcParam) {
    }

    public override decimal CalculateStockValue() {
        //过滤价格超过了上限的商品
        var filteredProducts = products.Where(e => e.Price < ItemLimit);

        return calculator.ValueProducts(filteredProducts.ToArray());
    }

    public decimal ItemLimit { get; set; }
}

然后把父类ShoppingCart绑定到子类LimitShoppingCart:

public static void Main() {
    IKernel ninjectKernel = new StandardKernel();

    ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>()
        .WithPropertyValue("DiscountSize", 5M);
    //派生类绑定
    ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>()
        .WithPropertyValue("ItemLimit", 3M);

    ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();
    Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
    Console.ReadKey();
}

运行结果:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

从运行结果可以看出,cart对象调用的是子类的CalculateStockValue方法,证明了可以把父类绑定到一个继承自该父类的子类。通过派生类绑定,当我们请求父类的时候,Ninject自动帮我们创建一个对应的子类的实例,并将其返回。由于抽象类不能被实例化,所以派生类绑定在使用抽象类的时候非常有用。

5.条件绑定

当一个接口有多个实现或一个类有多个子类的时候,我们可以通过条件绑定来指定使用哪一个实现或子类。为了演示,我们给IValueCalculator接口再添加一个实现,如下:

public class IterativeValueCalculator : IValueCalculator { 
 
    public decimal ValueProducts(params Product[] products) { 
        decimal totalValue = 0; 
        foreach (Product p in products) { 
            totalValue += p.Price; 
        } 
        return totalValue; 
    } 
}

IValueCalculator接口现在有两个实现:IterativeValueCalculator和LinqValueCalculator。我们可以指定,如果是把该接口的实现注入到LimitShoppingCart类,那么就用IterativeValueCalculator,其他情况都用LinqValueCalculator。如下所示:

public static void Main() {
    IKernel ninjectKernel = new StandardKernel();

    ninjectKernel.Bind<IValueCalculator>().To<LinqValueCalculator>();
    ninjectKernel.Bind<IDiscountHelper>().To<DefaultDiscountHelper>()
        .WithPropertyValue("DiscountSize", 5M);
    //派生类绑定
    ninjectKernel.Bind<ShoppingCart>().To<LimitShoppingCart>()
        .WithPropertyValue("ItemLimit", 3M);
    //条件绑定
    ninjectKernel.Bind<IValueCalculator>()
        .To<IterativeValueCalculator>().WhenInjectedInto<LimitShoppingCart>();

    ShoppingCart cart = ninjectKernel.Get<ShoppingCart>();
    Console.WriteLine("Total: {0:c}", cart.CalculateStockValue());
    Console.ReadKey();
}

运行结果:

[ASP.NET MVC Mavericks Road] 04 - Dependency Injection (DI) and Ninject

在ASP.NET MVC中使用Ninject

本文用控制台应用程序演示了Ninject的使用,但要把Ninject集成到ASP.NET MVC中还是有点复杂的。首先要做的事就是创建一个继承System.Web.Mvc.DefaultControllerFactory的类,MVC默认使用这个类来创建Controller类的实例(后续博文会专门讲这个)。代码如下:


NinjectControllerFactory

现在暂时不解释这段代码,大家都看懂就看,看不懂就过,只要知道在ASP.NET MVC中使用Ninject要做这么一件事就行。

添加完这个类后,还要做一件事,就是在MVC框架中注册这个类。一般我们在Global.asax文件中的Application_Start方法中进行注册,如下所示:

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);

    ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
}

注册后,MVC框架就会用NinjectControllerFactory类去获取Cotroller类的实例。在后续博文中会具体演示如何在ASP.NET MVC中使用Ninject,这里就不具体演示了,大家知道需要做这么两件事就行。

虽然我们前面花了很大功夫来学习Ninject就是为了在MVC中使用这样一个NinjectControllerFactory类,但是了解Ninject如何工作是非常有必要的。理解好了一种DI容器,可以使得开发和测试更简单、更高效。

 以上就是[ASP.NET MVC 小牛之路]04 - 依赖注入(DI)和Ninject的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn