控制反转(Ioc)和依赖注入(DI)
控制反转IOC, 全称 “Inversion of Control”。依赖注入DI, 全称 “Dependency Injection”。
一个简单的场景:
当一个类的实例需要另一个类的实例协助时,在传统的程序设计过程中,通常有调用者来创建被调用者的实例, 并使用。
面向的问题:
软件开发中,为了降低模块间、类间的耦合度,提倡基于接口的开发,那么在实现中必须面临最终是有“谁”提供实体类的问题。(将各层的对象以松耦合的方式组织起来,各层对象的调用面向接口。)
理解将组件的配置和使用分离:
如果觉得这句话比较抽象, 可以将"组件"理解为"对象"(底层组件),那么相应的“组件的配置”就可以理解成为“对象的初始化”, 再来理解这句话,就是将对象的创建和使用分离.其优点很明显, 将对象的创建延迟到部署阶段(这句话也可能不太好理解), 就是说对象的创建全部依赖于统一的配置(声明), 这样我们就可以通过修改配置动态地把我们期望使用的类替换成我们打算使用的类,而不修改任何使用对象的原有代码.原则上,我们需要把对象的装配(配置/声明)和业务代码(使用)分离开来.
软件系统中的依赖(耦合):
在采用面向对象设计的软件系统中,万物皆对象,所有的对象通过彼此的合作,完成整个系统的工作.就好比下面的齿轮,每个齿轮转动才能保证整个齿轮系统的运转.但是这样的设计就意味着强依赖,强耦合. 如果某个齿轮出现问题了(发生改变), 整个齿轮系统可能就会瘫痪, 这显然是不能接受的.
什么是控制反转(IOC)?
耦合关系不仅会出现在对象与对象之间, 也会出现在软件系统的各个模块之间,以及软件系统和硬件系统之间.如何降低系统之间, 模块之间和对象之间的耦合度, 是软件工程永远追求的目标.
为了解决对象之间的耦合度过高的问题, 软件专家Michael Mattson提出了IOC理论, 用来实现对象之间的"解耦". 目前这个理论已经被成熟的应用到项目开发中, 衍生出了各式各样的IOC框架产品.
IOC理论提出的大致观点是这样的: 借助于"第三方"实现具有依赖关系的对象之间的解耦, 如下图:
由于引进了中间位置的"第三方",这里成为IOC容器(Container), 使得A, B, C, D这4个对象没有了耦合关系, 齿轮之间的传动全部依靠"第三方"了, 换句话说, 全部对象的控制权全部上缴给"第三方"IOC容器,
所以, IOC容器成了整个系统的关键核心, 它将对象之间的依赖关系降低到了最低程度.
为了在详细说明其中的差别, 我们来对比下: 在没有引入IOC容器之前, 第一张图中, 对象A依赖于对象B, 那么A在初始化或者运行到某一点的时候, A自己必须主动创建对象B或者直接使用对象B. 而无论是创建还是使用对象B, 控制权在A自己手上.
软件开发引入IOC容器之后, 这种情况就完全不同了, 第二章图中, 由于IOC容器的加入, 对象A和B之间失去了直接联系, 所以, 当对象A运行到需要对象B的时候, IOC容器主动创建一个对象B, 并将其注入到对象A需要的地方.
通过前后的对比, 也就不难看出: 对象A获得依赖对象B的过程, 由主动行为变为被动行为, 控制权发生转换, 这就是"控制反转"的由来.
什么是依赖注入(DI)?
2004年, Martin Fowler,在其著名的文章《Inversion of Control Containers and the Dependency Injection pattern》探讨了同一个问题, 既然IOC是控制反转, 那么到底"那些方面的控制需要被反转呢?' 经过详细地分析和论证后,
他得出了答案: “获得依赖对象的过程需要反转”. 控制被反转之后, 获得依赖对象的过程由自身管理变为了由IOC容器主动注入. 于是,他给"控制反转"取了一个更适合的名称叫做"依赖注入 Dependency Injection"。
他的这个答案, 实际上给出了实现IOC的方法: 依赖注入. 所谓依赖注入, 就是由IOC容器在运行期间, 动态地将某种依赖关系注入到对象之中.
也就是说: 采用依赖注入的方式,创建被调用者的实例的工作不再由调用者完成,而是由IOC容器来完成,这就是“控制反转”的意思,然后,将其注入调用者,因此也称为 “依赖注入”。
我们也可以知晓, 控制反转(IOC) 和 依赖注入(DI) 是从不同角度对同一件事务的描述. 就是通过IOC容器, 利用注入依赖关系的方式, 实现对象之间的解耦.
Martin Fowler在文中将具体依赖注入划分为三种形式,即构造器注入、属性(设置)注入和接口注入。
习惯将其划分为一种(类型)匹配和三种注入:
- 类型匹配(Type Matching):虽然我们通过接口(或者抽象类)来进行服务调用,但是服务本身还是实现在某个具体的服务类型中,这就需要某个类型注册机制来解决服务接口和服务类型之间的匹配关系;
- 构造器注入(Constructor Injection):IoC容器会智能地选择选择和调用适合的构造函数以创建依赖的对象。如果被选择的构造函数具有相应的参数,IoC容器在调用构造函数之前解析注册的依赖关系并自行获得相应参数对象;
- 属性注入(Property Injection):如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后,IoC容器会自动初始化该属性;
- 方法注入(Method Injection):如果被依赖对象需要调用某个方法进行相应的初始化,在该对象创建之后,IoC容器会自动调用该方法
创建一个控制台程序,定义如下几个接口(IA、IB、IC和ID)和它们的实现类(A、B、C、D)。在类型A中定义了三个属性B、C和D,其参数类型分别为IB、IC和ID。
其中,
属性B作为构函数的参数,认为它会以构造器注入的方式被初始化 (??);
属性C应用了DependencyAttribute特性,意味着这是一个需要以属性注入方式被初始化的依赖属性;
属性D则通过方法Initialize初始化,该方法上应用了特性InjectionMethodAttribute, 意味着这是一个方法注入,在A对象被Ioc容器创建的时候,D会被自动调用。
Microsoft有一个轻量级的IoC框架Unity, 支持构造器注入,属性注入,方法注入。对于C#语言,由于语法元素上本身较其他语言丰富许多,如何实施注入还有些技巧和特色之处。
下面介绍如下:
测试类:
namespace UnityDemo { public interface IA { } public interface IB { } public interface IC { } public interface ID { } public class A : IA { public IB B { get; set; } [Dependency] public IC C { get; set; } public ID D { get; set; } public A(IB b) { this.B = b;
// I am A } [InjectionMethod] public void Initialize(ID d) { this.D = d; } } public class B : IB { // I am B } public class C : IC { // I am C } public class D : ID { // I am D } }
配置注册:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/> </configSections> <unity> <containers> <container name="defaultContainer"> <register type="UnityDemo.IA, UnityDemo" mapTo="UnityDemo.A, UnityDemo"/> <register type="UnityDemo.IB, UnityDemo" mapTo="UnityDemo.B, UnityDemo"/> <register type="UnityDemo.IC, UnityDemo" mapTo="UnityDemo.C, UnityDemo"/> <register type="UnityDemo.ID, UnityDemo" mapTo="UnityDemo.D, UnityDemo"/> </container> </containers> </unity> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration>
Main方法中,创建一个IOC容器的UnityContainer对象,并加载配置信息对其初始化,然后调用它的泛型的Resolve()方法创建一个实现了泛型接口IA的对象。
最后将返回对象转换成类型A, 并逐一检验B,C和D属性是否为空,即初始化情况。
namespace UnityDemo { class Program { static void Main(string[] args) { var container = new UnityContainer(); var configuration = ConfigurationManager.GetSection(UnityConfigurationSection.SectionName) as UnityConfigurationSection; configuration.Configure(container, "defaultContainer"); A a = container.Resolve<IA>() as A; if (null != a) { Console.WriteLine("a.B==null? {0}", a.B == null ? "Yes" : "No"); Console.WriteLine("a.C==null? {0}", a.C == null ? "Yes" : "No"); Console.WriteLine("a.D==null? {0}", a.D == null ? "Yes" : "No"); } } } }
执行结果:
分别体现了接口注入、构造器注入(属性B)、属性注入(属性C)和方法注入(属性D)。
依赖注入的技术点
IOC中最基本的技术就是"反射 (Reflection)" 编程. 关于反射的概念, 通俗地讲代码运行阶段, 根据给出的信息动态的生成对象.
JACK D. @ NJ USA