[ASP.NET
MVC Mavericks Road]04 - 종속성 주입(DI) 및 Ninject
이 문서의 디렉터리:
왜 종속성 주입 필요
MVC 패턴 이해에 대한 [ASP.NET MVC Mavericks Road] 시리즈 기사에서 MVC의 중요한 기능은 관심사 분리라고 언급했습니다. 우리는 애플리케이션의 다양한 구성 요소가 가능한 한 독립적이고 서로에 대한 종속성을 최소화하기를 원합니다.
이상적인 상황은 구성 요소가 다른 구성 요소를 모르거나 신경 쓰지 않지만 제공된 공개 인터페이스를 통해 다른 구성 요소의 함수 호출을 실현할 수 있다는 것입니다. 이러한 상황을 느슨한 결합이라고 합니다.
간단한 예를 들어보세요. 제품에 대한 "고급" 가격 계산기 LinqValueCalculator를 사용자 정의하려고 합니다. 이 계산기는 IValueCalculator 인터페이스를 구현해야 합니다. 다음 코드에 표시된 대로:
public interface IValueCalculator { decimal ValueProducts(params Product[] products); } public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { return products.Sum(p => p.Price); } }
Product 클래스는 처음 두 블로그 게시물에 사용된 클래스와 동일합니다. 이제 장바구니에 있는 항목의 총 가격을 계산할 수 있는 기능이 필요한 장바구니 ShoppingCart 클래스가 있습니다. 그러나 장바구니 자체에는 계산 기능이 없으므로 장바구니에 계산기 구성 요소가 포함되어야 합니다. 이 계산기 구성 요소는 LinqValueCalculator 구성 요소일 수 있지만 반드시 LinqValueCalculator 구성 요소일 필요는 없습니다. , 기타 고급 계산이 내장될 수 있습니다. 그런 다음 장바구니 ShoppingCart 클래스를 다음과 같이 정의할 수 있습니다.
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 }
ShoppingCart 클래스는 LinqValueCalculator 인터페이스가 아닌 IValueCalculator 인터페이스를 통해 제품의 총 가격을 계산합니다. 나중에 장바구니를 업그레이드할 때 좀 더 고급 계산기를 사용해야 한다면 코드 10번째 줄에서 new 이후 객체만 변경하면 되고(즉, LinqValueCalculator 교체) 다른 코드에서는 그럴 필요가 없습니다. 변경됩니다. 이는 어느 정도의 느슨한 결합을 달성합니다. 이때 이 세 가지의 관계는 아래 그림과 같습니다.
이 그림은 ShoppingCart 클래스가 IValueCalculator 인터페이스와 LinqValueCalculator 클래스 모두에 종속되어 있음을 보여줍니다. 여기에는 문제가 있습니다. 현실적으로 장바구니에 내장된 계산기 구성요소가 파손되면 장바구니 전체가 제대로 작동하지 않게 된다는 의미는 아닙니다. ? 가장 좋은 방법은 계산기 구성 요소와 장바구니를 완전히 분리하여 어떤 구성 요소가 파손되더라도 해당 구성 요소만 교체하면 되는 것입니다. 즉, 우리가 해결하고 싶은 문제는 ShoppingCart 컴포넌트와 LinqValueCalculator 컴포넌트를 완전히 분리하는 것이고, 의존성 주입의 디자인 패턴은 이 문제를 해결하는 것입니다.
종속성 주입이란
위에서 달성한 부분 느슨한 결합은 분명히 우리에게 필요한 것이 아닙니다. 우리에게 필요한 것은 클래스 내에서 객체의 인스턴스를 생성하지 않고도 공용 인터페이스를 구현하는 객체에 대한 참조를 얻을 수 있는 것입니다. 이러한 "필요"를 DI(Dependency Injection)라고 하며, 소위 IoC(Inversion of Control)와 같은 의미를 갖습니다.
DI는 인터페이스를 통해 느슨한 결합을 구현하는 디자인 패턴입니다. 초보자들은 DI에 초점을 맞춘 기술 기사가 인터넷에 왜 그렇게 많이 있는지 궁금해할 것입니다. DI는 개발자가 거의 모든 프레임워크에서 애플리케이션을 효율적으로 개발해야 하는 중요한 개념이기 때문입니다. 분리하는 중요한 수단입니다.
DI 모드는 두 부분으로 나눌 수 있습니다. 하나는 구성 요소(위 예에서는 LinqValueCalculator)에 대한 종속성을 제거하는 것이고, 다른 하나는 클래스의 생성자(또는 클래스의 Setter 접근자)를 통해 공용 인터페이스를 구현하는 구성 요소에 대한 참조를 전달하는 것입니다. 다음 코드에서 볼 수 있듯이
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; } }
이런 방식으로 ShoppingCart와 LinqValueCalculator 간의 종속성을 완전히 분리합니다. IValueCalculator 인터페이스(예제에서는 LinqValueCalculator)를 구현하는 클래스의 인스턴스 참조가 ShoppingCart 클래스의 생성자에 매개 변수로 전달됩니다. 그러나 ShoppingCart 클래스는 IValueCalculator 인터페이스를 구현하는 클래스를 모르거나 신경 쓰지 않으며 이 클래스를 작동할 책임이 없습니다. 이때 다음 그림을 사용하여 ShoppingCart, LinqValueCalculator 및 IValueCalculator 간의 관계를 설명할 수 있습니다.
在程序运行的时候,依赖被注入到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”),如下图:
在弹出如下窗口中选择“联机”,搜索“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的使用的一般步骤就是这样。该示例可正确输出如下结果:
但看上去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之间的关系:
以此,我们还可以添加更多的接口和实现接口的类,接口和类越来越多时,它们的关系图看上去会像一个依赖“链”,和生物学中的分子结构图差不多。
按照前面说的使用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 }
输出结果:
代码一目了然,虽然新添加了一个接口和一个类,但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)。运行结果如下:
如果要给多个属性赋值,则可以在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(); }
运行结果:
从运行结果可以看出,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中使用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中文网(m.sbmmt.com)!