IoC 容器和Dependency Injection 模式》一文Martin Fowler先生用了较大的篇幅来比较了依赖注入(Dependency Injection) 模式和服务定位器(Service Locator)模式,并提出一个二者都遵循的的原则:应该将应用和配置分离.作为依赖注入方面的经典之作这篇文章为我们学习给出了一个个路标,我们可以根据自己的实践来将深入理解作者的观点,无论你使用的是Spring.net还是Unity.本文就是我使用Unity一段时间之后按照文中的指引做的一个简单的回顾.

IoC 容器和Dependency Injection 模式》首先对"容器们"做了一个高度的概括:1.相比较大型框架,"容器们"是轻量级的2.这些容器能将来自不同项目的逐渐集结为一个内聚的应用程序3."容器们"都是基于同一个模式,Ioc模式,并为这个模式的工作机制定名为:依赖注入.

     Unity是微软的轻量级可扩展依赖注入容器,在展开一些细节之前首先回答一个问题:你是不是真的需要Unity?如果你的应用程序依赖其它类而这种依赖又是十分复杂的,那么这种依赖就需要抽象.你需要在运行时配置和修改这种依赖并进行对象生命周期的管理那么Unity是一个很好的选择.如果你的类不存在依赖或者依赖很简单不需要做抽象那就没有必要引入Unity.

      下面我们根据文中提到的路标对Unity做一个简单的梳理.

 

>>依赖注入的形式

>>>构造器注入

         使用构造器注入,创建一个对象的时候把它依赖的对象同时创建出来.如果是单个构造函数注入可以自动完成,如果是多个构造函数,就需要使用[InjectionConstructor]来做标记.如果多个构造函数而又没有使用InjectionConstructor标记,Unity会选择参数最多的那个构造函数,要是找不到符合这样条件的构造函数就要抛出异常了.

      如果使用RegisterInstance方法注册一个已经存在的对象,构造函数注入执行过程中对于这个对象的注入是不会执行的,因为这个对象已经在容器外部完成了创建.即使调用容器的BuildUp方法并传入已经存在的对象,构造函数注入也不会执行.不过可以使用Dependency标签来做强制注入.

     使用构造器注入帮助你把保持应用程序的封闭,把改动放在依赖的对象里.如果你不想暴露出来属性或者方法,构造器注入也是一个很好的选择.

 

>>>属性注入

属性注入是通过在属性上添加[Dependency]标记来完成的,不仅如此,还可以使用该标记完成名称映射.例如[Dependency("AnotherName")]这是用在什么用途呢?比如一个类里面有两个属性都是同一个抽象类型.但是需要解析成不同实例,就需要使用Name属性了.

public class MyObject
{
  private IMyInterface _objA, _objB;
  [Dependency("MapTypeA")]
  public IMyInterface ObjectA
  {
    get { return _objA; }
    set { _objA = value; }
  }
  [Dependency("MapTypeB")]
  public IMyInterface ObjectB
  {
    get { return _objB; }
    set { _objB = value; }
  }
}

 

容器相关的代码:

IUnityContainer uContainer = new UnityContainer()
   .RegisterType<IMyInterface, FirstObject>("MapTypeA")
   .RegisterType<IMyInterface, SecondObject>("MapTypeB");
MyObject myInstance = uContainer.Resolve<MyObject>();
上面在构造器注入一节已经提到可以通过[Dependency]来做强制注入:

public StoplightSchedule([Dependency] IStoplightTimer timer)
{
  this._timer = timer;
}

同样的,对于使用RegisterInstance方法注册一个已经存在的对象,属性注入执行过程中这个对象的注入是不会执行的,因为这个对象已经在容器外部完成了创建.
 

>>>方法注入vs属性注入 构造器注入

        方法注入使用的标记是[InjectionMethod],方法注入和构造器注入,属性注入有着同样的要点:对于使用RegisterInstance方法注册一个已经存在的对象,方法注入执行过程中这个对象的注入是不会执行的,因为这个对象已经在容器外部完成了创建.

        这三种方式都要注意避免循环引用,循环引用以下面的形式出现:

        对象通过构造器注入创建,两个对象互为对方的构造器参数. 对象通过构造器创建,对象的实例作为参数传入自己的构造器.对象通过方法构造注入,并且对象间互相引用对方.对象通过属性注入创建,对象互相引用.

         方法注入的侵入性要比属性注入和构造器注入的方式强,这就是为什么轻量级容器都致力于研究属性注入和构造器注入的原因.属性注入和构造器注入之间还有一个选择,Martin先生的实践经验是把对象的创建放在构造阶段.如果构造函数过于复杂或者对象之间存在复杂的继承关系,构造器注入就会出现问题:为了能够正确初始化你必须将子类的构造器调用转到副构造器然后处理自己的参数.这可能导致构造器爆炸式膨胀.尽管诸多缺陷我还是首推构造器注入,不过我们已经做好准备一旦遇到上述的问题就转向属性注入.在以依赖注入作为框架核心的几个开发团队之间,属性注入还是构造器注入这个话题上争论不休。不过多数人都已经意识到即使有所偏重还是要兼顾两种注入方式。

 

 

>>配置文件还是代码?

Unity完全支持使用代码配置和XML配置,Martin先生文章的一再强调:要把配置从应用程序中分离出来.我个人也倾向于把配置放在XML.Unity配置文件的编写说明:

ms-help://MS.VSCC.v90/MS.VSIPCC.v90/ms.practices.entlib.2008may/EntLib4Docs/html/EntLib2008May_1bdc5fd2-7ebf-4766-9b5a-eb0dceaeff95.html

    Unity配置文件你会发现,如果有多个容器,它们在配置文件中的位置中的位置是平行的没有嵌套的情况.如何表达容器间的嵌套呢?这个要在代码里做了,例如:

 

IUnityContainer parentContainer = new UnityContainer();
IUnityContainer childContainer = parentContainer.CreateChildContainer();
UnityConfigurationSection section
  = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
section.Containers["containerOne"].GetConfigCommand().Configure(parentContainer);
section.Containers["nestedChildContainer"].Configure(childContainer);

 

>>对象的生命周期?

IoC 容器和Dependency Injection 模式》文中的最后作者说:"还有一些和这些容器相关的话题需要注意,我已经没有时间继续深入挖掘了下去了。特别注意的是生命周期的问题:某些组件具有特定的生命周期事件."

今天我们就把这个话题继续下去.

RegisterType 每一次都会创建一个实例,容器并不存储对象的引用.如果你要使用Unity的单件创建行为,容器就会维护对象的应用并管理这些对象的生命周期.

如果一个类继承了LifetimeManager它就要完成对象实例的存储和销毁.

如果你使用RegisterInstance方法,默认的行为是容器使用ContainerControlledLifetimeManager接管了该对象的生命周期,这就意味着这个对象的生命周期从属于容器直到容器被GC回收或者使用代码显示销毁.你可以通过指定Unity按照单件的方式管理对象来使用这个生命周期管理器.

Unity有两个生命周期管理器ContainerControlledLifetimeManagerExternallyControlledLifetimeManager,你也可以根据需要创建自己的生命周期管理器.使用ContainerControlledLifetimeManager每一次进行注入时都会返回统一实例,它为对象实现了单件模式调用,RegisterInstance方法默认使用ContainerControlledLifetimeManager.如果是使用RegisterType方法就要在代码中显示定义使用该生命周期管理器,这样Unity会在第一次进行注入的时候创建该类的实例后续的请求会返回同一实例.

ExternallyControlledLifetimeManager允许你注册类映射或者对象实例到容器并维护一个弱引用.第一次注入是创建对象之后之后就会返回同一对象.但是容器并没有持有对象的强引用,如果其它对象也没有持有该对象的强引用GC可以销毁该对象.

 

 

>>最后:控制反转,反转了什么?

这个问题放在最后:控制反转,反转了什么?作者说控制反转是所有框架的共有特征,又怎么理解这句话呢?

借用文中的例子来说明一下:早期的时候通过命令行来提示用户输入,而现在我们依赖GUI框架,我们只需要为特定事件编写代码而无须考虑界面代码.

控制反转我的理解是:责任的转移,责任从应用程序转移到了框架中.那么什么责任转移到那些轻量级的Ioc框架中了呢?

 

用下面的图简单回顾一下作者的例子:

 

     我们分析一下目前的状况:

      1. 问题关键所在:MoiveFinder是一个不确定的因子,作者使用的使用文本文件中查找.而使用者有可能使用数据库或者xml等等.因而就不能在编译时就确定MoiveFinder.对于这种不确定我们首先想到的方法就是抽象出来一个接口,作者的第一步也是这么做的,于是便有了上面的图.

      2.上图所示:虽然加入了一个接口,但是MovieLister类同时依赖于MoiveFinder接口及其实现,接口的引入并没有什么改善.

     3.MovieLister类负责MovieFinder的创建:它直接就实例化了一个子类。MoiveFinder不是在运行时加载的,也就不具备可插入性。

       当把所有的关键点列出来之后,我们的目标也就明确了:我们的设计要使MoiveLister类能在运行时自动加载任何MoiveFinder实现并与其配合工作。

      要实现这个目标我们就要把创建MoiveFinder的责任从MoiveLister中转移出来.责任转移到哪里呢?,依赖注入容器.

   把可插入对象的创建责任从应用程序转移到依赖注入中,这就是依赖注入容器关注的控制反转.控制反转既然是每一个框架的特点,作者给这种特殊控制反转了一个特定的名字:依赖注入.
Toolkit