[ASP.NET
MVC Mavericks Road]04 – Dependency Injection (DI) und Ninject
Verzeichnis dieses Artikels:
Warum Erfordert Abhängigkeitsinjektion
In der Artikelreihe [ASP.NET MVC Mavericks Road] zum Verständnis des MVC-Musters haben wir erwähnt, dass ein wichtiges Merkmal von MVC die Trennung von Belangen ist. Wir möchten, dass die verschiedenen Komponenten der Anwendung möglichst unabhängig sind und möglichst wenig Abhängigkeiten voneinander aufweisen.
Unsere Idealsituation ist: Eine Komponente kennt andere Komponenten nicht oder kümmert sich nicht um sie, kann aber die Funktionsaufrufe anderer Komponenten über die bereitgestellte öffentliche Schnittstelle realisieren. Diese Situation wird als lose Kopplung bezeichnet.
Geben Sie ein einfaches Beispiel. Wir möchten einen „erweiterten“ Preisrechner LinqValueCalculator für das Produkt anpassen. Dieser Rechner muss die IValueCalculator-Schnittstelle implementieren. Wie im folgenden Code gezeigt:
public interface IValueCalculator { decimal ValueProducts(params Product[] products); } public class LinqValueCalculator : IValueCalculator { public decimal ValueProducts(params Product[] products) { return products.Sum(p => p.Price); } }
Die Produktklasse ist dieselbe wie die, die in den ersten beiden Blogbeiträgen verwendet wurde. Jetzt gibt es eine Warenkorb-Klasse ShoppingCart, die über eine Funktion verfügen muss, die den Gesamtpreis der Artikel im Warenkorb berechnen kann. Der Warenkorb selbst verfügt jedoch nicht über eine Berechnungsfunktion. Daher muss der Warenkorb eine Rechnerkomponente einbetten. Diese Rechnerkomponente kann eine LinqValueCalculator-Komponente sein, aber nicht unbedingt eine LinqValueCalculator-Komponente (zukünftig, wenn der Warenkorb aktualisiert wird). , andere fortgeschrittenere Berechnungen können eingebettet sein. Dann können wir die ShoppingCart-Klasse für den Warenkorb wie folgt definieren:
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 }
Die ShoppingCart-Klasse berechnet den Gesamtpreis des Produkts über die IValueCalculator-Schnittstelle (nicht über LinqValueCalculator). Wenn Sie beim späteren Aktualisieren Ihres Warenkorbs einen erweiterten Rechner verwenden müssen, müssen Sie das Objekt nur in der 10. Codezeile nach dem anderen ändern (d. h. den LinqValueCalculator ersetzen), der andere Code ist nicht erforderlich geändert werden. Dadurch wird ein gewisser Grad an loser Kopplung erreicht. Zu diesem Zeitpunkt ist die Beziehung zwischen den drei wie in der folgenden Abbildung dargestellt:
Diese Abbildung zeigt, dass die ShoppingCart-Klasse sowohl von der IValueCalculator-Schnittstelle als auch von der LinqValueCalculator-Klasse abhängt. In der Praxis gibt es ein Problem: Wenn die im Warenkorb integrierte Rechnerkomponente kaputt geht, funktioniert das nicht richtig. Bedeutet das nicht, dass der gesamte Warenkorb ersetzt werden muss? ? Der beste Weg ist, die Rechnerkomponente und den Warenkorb vollständig zu trennen, sodass Sie unabhängig davon, welche Komponente defekt ist, nur die entsprechende Komponente ersetzen müssen. Das heißt, das Problem, das wir lösen möchten, besteht darin, die ShoppingCart-Komponente und die LinqValueCalculator-Komponente vollständig zu trennen, und das Entwurfsmuster der Abhängigkeitsinjektion soll dieses Problem lösen.
Was ist Abhängigkeitsinjektion?
Die oben erreichte teilweise lose Kopplung ist offensichtlich nicht das, was wir brauchen. Was wir brauchen, ist, innerhalb einer Klasse einen Verweis auf ein Objekt zu erhalten, das eine öffentliche Schnittstelle implementiert, ohne eine Instanz des Objekts zu erstellen. Dieses „Bedürfnis“ wird DI (Dependency Injection) genannt, was die gleiche Bedeutung hat wie das sogenannte IoC (Inversion of Control).
DI ist ein Entwurfsmuster, das eine lose Kopplung über Schnittstellen erreicht. Anfänger fragen sich vielleicht, warum es im Internet so viele technische Artikel gibt, die sich auf DI konzentrieren. Denn DI ist ein wichtiges Konzept, das Entwickler benötigen, um Anwendungen unter fast allen Frameworks effizient zu entwickeln. Es ist ein wichtiges Mittel zur Entkopplung.
Der DI-Modus kann in zwei Teile unterteilt werden. Eine besteht darin, die Abhängigkeit von der Komponente zu entfernen (LinqValueCalculator im obigen Beispiel), und die andere darin, über den Konstruktor der Klasse (oder den Setter-Accessor der Klasse) einen Verweis auf die Komponente zu übergeben, die die öffentliche Schnittstelle implementiert. Wie im folgenden Code gezeigt:
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; } }
Auf diese Weise trennen wir die Abhängigkeit zwischen ShoppingCart und LinqValueCalculator vollständig. Eine Instanzreferenz einer Klasse, die die IValueCalculator-Schnittstelle implementiert (im Beispiel LinqValueCalculator), wird als Parameter an den Konstruktor der ShoppingCart-Klasse übergeben. Aber die ShoppingCart-Klasse kennt die Klasse, die die IValueCalculator-Schnittstelle implementiert, nicht und kümmert sich nicht um sie, und sie ist nicht für den Betrieb dieser Klasse verantwortlich. Zu diesem Zeitpunkt können wir die folgende Abbildung verwenden, um die Beziehung zwischen ShoppingCart, LinqValueCalculator und IValueCalculator zu beschreiben:
在程序运行的时候,依赖被注入到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)!