依赖注入实践篇

目录 

1. IoC使用简介与原理

  1.1 依赖关系

  1.2 面向接口编程

  1.3 IoC使用与实现原理

2. 模式与经验

  2.1 code configuration vs. xml configuration

  2.2 IoC容器依赖

1. IoC使用简介与原理

1.1 依赖关系

在面向对象编程中,类与类之间总是要有一些依赖关系,有些依赖强一些,有些依赖弱一些,常见的依赖关系有:

o 继承依赖 - 子类依赖于父类
o 实现依赖 - 类依赖于接口
o 参数依赖 - 当类的一个方法有其它类型的参数时
o 其它 - 当类中的字段为其它类型,或者代码中有对其它类型的使用。

其中参数依赖是相对较弱的依赖关系,而继承依赖则是较强的依赖关系。判断两个类是否有依赖关系有一个很简单的办法,比如想知道类A是否依赖于类B,只要把B删掉然后编译一下,如果编译错误就证明A依赖于B -- 当然,删掉B之前代码应该是编译正确的。

1.2 面向接口编程

看一下这段代码:

Code

其中,A对B有依赖关系,因为B是一个具体类,如果需要添加或改变B的行为则需要直接改变B的代码,同时B和A都需要重新编译,这就违反了开放封闭原则。我们可以修改一下代码,让A依赖于一个接口IFoo,而B则实现这个接口。这样A与B就没有依赖关系了,他们都只依赖于IFoo。这时只需要添加一个实现IFoo的类,再把类的实例传给A就可以改变IFoo的实现和行为。代码如下:

Code

1.3 IoC使用与实现原理

当我们需要使用A的时候,就需要实例化A,最简单的方式是直接用new

A a = new A(new B());

另一种方式是使用IoC

IUnityContainer myContainer = new UnityContainer();
myContainer.RegisterType
<IFoo, B>();
IFoo foo 
= myContainer.Resolve<IFoo>();
A a 
= new A(foo);

这里调用了Unity的两个主要方法,一个是RegisterType<IService, Service>,用来注册一个服务和他的实现;另一个是Resolve<IService>,用来得到实现IService的类的实例。当我调用myContainer.Resolve<IFoo>()的时候,Unity会发现IFoo是一个接口,那么Unity就会寻找他的实现类,也就是B因为我们已经注册了,接着Unity会使用类似Activator.CreateInstance这样的方法来实例化B,并返回这个实例。

由于IoC一般都支持嵌套的依赖关系,因此我们完全不必自己来实例化B,可以由容器为我们自动解决。

IUnityContainer myContainer = new UnityContainer();
myContainer.RegisterType
<IFoo, B>();
A a 
= myContainer.Resolve<A>();

在这里,我们直接向容器请求A的实例。大概的步骤如下:

1. 由于A是一个具体类,因此不需要寻找他的实现类,直接实例化A就可以了

2. 查看A的依赖,发现构造函数中有对IFoo的依赖,因此实例化A之前,需要先实例化IFoo

3. 由于IFoo是一个接口,因此需要寻找他的实现类,由于我们注册了B为IFoo的实现类,因此会实例化B

4. 由于B是一个具体类,而且没有对其它类的依赖关系,因此直接实例化B

5. A的所有依赖都得到了实例化,将B的实例传给A的构造函数来实例化A

6. 返回A的实例

可以看到,IoC的实例化过程是一个递归的过程。每个类所依赖的类或接口都需要首先实例化,而这些类或接口又有可能依赖其它的类或接口,这样一直递归下去。假设有一个类C,依赖类D,而类D又依赖于E……最后Y依赖于Z。那么如果我向IoC容器要一个C的实例,IoC会先实例化Z,然后是Y,然后是X……E,D最后才是C。

由于IoC会自动解析接口与实现类的关系,并实例化正确的类,因此程序的代码完全不需要考虑接口的实例化问题,可以完全面向接口。而在IoC中注册类时,也可以通过注册不同的类,改变接口的行为。

例如,如果我们还有一个IFoo的实现类C。我们想用C来代替当前的IFoo实现B,只需要修改一行代码

myContainer.RegisterType<IFoo, B>();
替换成
myContainer.RegisterType
<IFoo, C>();

这样,A就会使用C的实现了。

除了构造函数依赖,Unity也支持其它的依赖,详细的API和说明请参考文档。

PS: 我认为Unity有一个比Castle好的特性,就是实例化一个具体类时,不需要注册。如果用Castle的话,上面的示例还要加上类似于RegisterType<B>()这样的代码。

2.模式与经验

2.1 code configuration vs. xml configuration 

目前的IoC容器一般都会支持代码配置和Xml配置两种配置,代码配置就是向前面的示例那样,在代码中注册服务接口与实现,而Xml配置则是在Xml或者.config文件中配置。由于从.NET诞生的那一天起,.config文件就是一个推荐的配置地点,因此很多人也喜欢将配置信息放到.config文件中去,然而代码配置的方式也有其优势,在这里我将比较一下两种方式的优劣。

在我看来,代码配置的方式使用起来非常简单,所需要的代码行数比较少,而且有自动完成,强类型,编译时检查。唯一的缺点是改变配置时,需要重新编译代码,而且必须有源代码才能修改。而xml配置的方式稍微复杂,所需的行数要多一些,没有编译时检查,需要运行的时候才能直到是否有错。优点是改配置不需要源代码,不需要重新编译,甚至可以在运行时修改。这两种配置方式的表达能力,在常用情况中几乎是等价的(代码配置好像要多一些)。

以我个人的经验,推荐在开发时使用代码配置,而在快发布时再改为xml配置。因为开发时,一切都是不确定的,代码配置的方式修改比较简单,而且还支持重构。想象一下,如果用xml配置,你用重构工具重命名了一个接口名,还要到xml中手动修改,这是多大的工作量啊。而在临近发布时,代码趋于稳定,修改的可能性很低,这时再放到xml中就可以充分利用xml的好处了。

2.2 IoC容器依赖 

我看到过有些人使用IoC的方式就是把new换成了Resolve而已,几乎每个类中需要new一个实例的时候,都会调一下容器的API。虽然解除了程序中类与类之间的依赖,但是这些类却都有对IoC容器的依赖。假如我现在使用的容器是Unity,而某一天我发现Unity满足不了我的需要,我要换一个容器时,由于代码中遍布着对Unity的直接依赖代码,替换容器的代价就会非常大。

这里有两个解决办法:

第一就是在程序代码和容器之间加一个层,你可以自己写一个ServiceLocator之类的类,来包装IoC容器。这样程序代码对IoC容器的依赖就转变成对ServiceLocator的依赖,如果想换一个容器的话,只要修改ServiceLocator的代码,调用另一种容器的API就行了,减少了很多工作量。由于现在多数IoC的能力和API是比较相似的,因此CodePlex上有一个项目叫CommonServiceLocator,它提供了一个公共的接口,作为这个中间层。但是我本人不推荐使用这个库,因为本身IoC的接口一点也不复杂,自己写一个都比学习这个要简单,还可以根据需要调整接口。

第二种办法就是减少对IoC容器的依赖,只在最高层代码或者初始化代码中调用容器。通常在一个应用程序中,都有一个入口,一般来说是Program类的Main方法,如果你用的是Winform或者WPF这样的框架,那么可能会有一个Initialize方法或事件,用于初始化你自己的代码。如果你的类层次设计良好,你会有一个最高层的类,这个类依赖于同层或底层的一些类,他依赖的类又依赖于更低层的类,而这个类本身却不被任何类所依赖。那么当你用容器创建这个类的实例时,相当于所有的类都会创建出来。还记得前面的递归创建么?这种方法适用于那些不需要延迟加载的程序,因为程序启动时就会创建所有类实例。

posted @ 2009-06-21 12:37  Nick Wang (懒人王)  阅读(2698)  评论(18编辑  收藏  举报