依赖注入
作为一条设计原则,依赖倒置原则(DIP)强调高层组件应该依赖于抽象而不是某个具体的实现或功能。控制反转(IoC)就是对依赖倒置原则的一个应用,用一段泛化的代码控制更加特定的外部组件的执行。
在一个控制反转解决方案中,方法通常会有一个或多个空缺点。每个空缺点的功能都由外部组件提供(静态的或者动态的),并通过 抽象接口调用。若满足了里氏替换原则和开放/封闭原则,那么改变任何外部组件都不会影响高层次的方法。外部组件和高层次方法可以分开开发。
控制反转的一个现实实例是Windows的shell扩展。当用户右键单击并选择“属性”命令时,Windows资源管理器会准备一个标准的对话框,随后执行一系列的控制反转。Windows将查看注册表,判断是否有已经注册过的自定义的属性页。若存在,Windows将通过一系列接口与这些扩展通信,并将页面添加至属性对话框中。
控制反转的另一个实现实例是事件驱动编程,起源于Visual Basic,现在已得到了Windows Form和Web Form的支持。例如,若编写了一个Button1_Click方法,并将其关联到Button1控件的Click事件上,那么也就是让Button类的代码(可重用且足够泛化)在用户单击时回调Button1_Click方法。
那么依赖注入(Dependency Injection,DI)又是什么呢?
从依赖倒置到依赖注入
在这里的讨论中,控制反转和依赖注入可以认为是同义词。不过在字面上二者并不总是同义词,控制反转有时会指导原则,而依赖注入则表明了原则的应用——即模式。实际上,控制反转历史上曾是基于依赖倒置原则的一个模式。依赖注入这个名词有Martin Fowler提出,用来专指控制反转的概念。
控制反转/依赖注入作为一个模式,可以让高层次方法不再需要辅助组件的具体引用。注入的过程可以通过三种方式来实现,一种是通过被注入方法所在的类的构造函数;第二种是通过在被注入方法所在的类中定义一个方法或者属性;最后一种是让被注入方法所在的类实现一个接口,接口中提供了辅助组件的具体实现。
目前,控制反转/依赖注入通常会随着某专门的框架提供,这些框架也提供了很多高级功能。
IoC框架
当前比较流行的几个Ioc框架。
Castle Windsor http://www.castleproject.org/container/index.html
Ninject http://www.ninject.org
Spring.Net http://www.springframework.net
StructureMap http://structuremap.sourceforge.net/Default.html
其中Ninject还可用于Silverlight和.NET Compact Framework。微软的Unity Application Block 是一个轻量级的IoC容器,支持构造函数、属性和方法的注入。Unity跟微软企业库一起发布。
所有的IoC容器框架有基于一个容器对象构造,这个容器对象将和一些配置信息绑定,并解析依赖。调用者代码将实例化容器,并将需要的接口以参数的形式传入,作为响应,Ioc/DI框架将返回一个实现了该接口的具体对象。
Ioc容器实战
假设某个类依赖于一个日志服务:
public class Task { private ILogger _logger; public Task(ILogger logger) { this._logger = logger; } public void Execute() { this._logger.Log("Begin method.."); this._logger.Log("End method.."); } }
Task类将通过构造函数接收日志组件,不过Task事故如何定位并初始化这个组件的呢?使用的静态new语句自然没有问题,这就要用到工厂。而IoC容器则是一个功能更加强大的框架,支持配置信息。
<configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <alias alias="ILogger" type="MyApp.ILogger, MyApp" /> <namespace name="MyApp.Implementations" /> <assembly name="MyApp" /> <container> <register type="ILogger" name="special" mapTo="DbLogger" /> </container> </unity> </configuration>
配置文件(app.config或者web.config)中将包含接口和将要注入的具体类型之间的映射。当容器调用ILogger时,将会返回一个DbLogger类型实例。
IUnityContainer container = new UnityContainer(); UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity"); section.Containers.Default.Configure(container); ILogger logger = container.Resolve<ILogger>(); Task t = new Task(logger);
对于测试而言,IoC/DI非常有用,因为它可以很容易地在不同的实现中切换。框架的使用让IoC/DI更加简单。通过脚本配置,可以让容器以单例的形式对待某个注入对象,这就意味着容器不会每次创建新的DbLogger实例,而是重用其唯一的实例。若DbLogger类是线程安全的话,那么的确能够大大提升效率。
此外,若DbLogger的构造函数需要引用到IoC/DI框架中注册的另一个类型,那么容器也能顺利地解析到该依赖。