[Prism]Composite Application Guidance for WPF(5)——依赖注入容器
周银辉
依赖注入容器和Prism的基础服务已经在本系列随笔中提到过很多次,今天将其分离出来专门说一说
1, 为什么要使用依赖注入容器
我们知道, 在Composite Application中各个模块之间是松耦合的关系, 也就是在设计的时候尽可能地减少模块间的依赖, 但无论如何从业务角度讲, 他们之间总还是要相互通信与合作的. 所以依赖注入容器在这其中扮演了一个桥梁般的角色. 比如当创建某一个组件实例的时候, 其依赖于另外的一个组件或服务, 此时依赖注入容器会将其需要的信息"注入(Injection)"进去, 采用注入的方式就避免了直接引用之间的耦合.除此之外, 使用依赖注入容器还有以下几方面的好处:
组件(Component)不必自己去定位其依赖项和维护依赖项的生命周期
替换组件的依赖项的具体实现时不会影响到组件代码
由于依赖项比较容易Mock,所以组件变得更易测
由于系统所需要的新的服务很容易被添加到容器中,系统维护难度也降低了
2, Prism的依赖注入容器
打开Composite Application Library(CAL)的源代码, 我们似乎没有找到一个依赖注入容器的实现,而是找到了一个依赖注入容器外观接口IContainerFacade,其利用了外观模式来高层抽象了一个依赖注入容器的最最基本的功能"解析"(Resolve).
/// <summary>
/// 定义一个简单的,被 Composite Application Library所使用的依赖注入容器外观.
/// </summary>
public interface IContainerFacade
{
/// <summary>
/// 从容器中解析出指定类型的实例
/// </summary>
/// <param name="type">从容器中获取的对象的类型.</param>
/// <returns>一个 <paramref name="type"/>类型的实例.</returns>
object Resolve(Type type);
/// <summary>
/// 尝试着从容器中解析出指定类型的实例
/// </summary>
/// <param name="type">从容器中获取的对象的类型.</param>
/// <returns>
/// 一个 <paramref name="type"/>类型的实例.
/// 如果类型不能被解析则返回<see langword="null" /> 值.
/// </returns>
object TryResolve(Type type);
}
事实上Patterns&Practices团队在设计时为了让Prism兼容更多的依赖注入容器(比如Spring.Net, Castle Windsor, Unity等等)而设计了这一接口,所以在Prism源代码中用到的都是IContainerFacade接口而不是一个具体的容器.
但是,在默认情况下,Prism使用了另外一个开源项目"Unity"中的轻量级的可扩展的依赖注入容器来作为默认容器UnityContainer. 该容器实现了构造函数注入、属性注入和方法注入三种注入方式。
关于Unity中的依赖注入容器,可以参考下面的这几篇文章:
深入 Unity 1.x 依赖注入容器之二:初始化 Unity
由于Unity不支持TryResolve(Type type);方法,Prism使用了一个适配器UnityContainerAdapter来解决这一问题。
3, 依赖注入容器的性能开销
由于将类型注册到依赖注入容器,然后使用容器来解析(Resolve)出类型实例对象时,根本机制是"反射"(Reflection),所以开销是比较大的, 那么如果你需要频繁地创建大量的实例的话,是否使用依赖注入容器是需要认真考虑的问题. 大量的依赖和很深的依赖嵌套也会导致性能问题. 另外,如果一个组件既没有依赖其它组件或服务,也不为其他组件所依赖,那么将其放到依赖注入容器中也是不明智的.
4, 是否注册为单例
我们在向依赖注入容器注册默认类型映射或注册Instance时涉及到一个问题是:是否按照单例模式注册. 如果是的话, 那么每次都容器中解析出来的对象都是同一对象实例,否则每次都是重新New的一个实例. 一般说来,对于一些全局服务以及共享状态等我们会将其注册为单例模式, 对于那些其被每次都需要拿一个新的实例去注入的依赖项我们将其按照非单例模式的形式注册.
5,使用IUnityContainer还是IContainerFacade
他们分别位于 Microsoft.Practices.Composite和Microsoft.Practices.Unity命名空间下, 虽然都可以作为容器的高层接口,但使用IUnityContainer基本上就意味着直接使用Unity容器(Unity项目中的那个依赖注入容器),而使用IContainerFacade则意味着你可以兼容格式各样的容器.但,我们知道IContainerFacade基本上只提供了一个功能"解析",但就一个基本的依赖注入容器而言,往往不仅仅是这一个功能,比如至少还要有类型注册、实例注册等等。所以在大多数情况下推荐使用IUnityContainer,从开源项目"StockTraderRI”中我们可以看到这一点,除非是下面的情况之一:
6, 代码配置 还是 配置文件配置
关于这个问题,我引用一下Martin Fowler 的一段话为大家提供一些参考:
“
代码配置 vs. 配置文件另一个问题相对独立,但也经常与其他问题牵涉在一起:如何配置服务的组装,通过配置文件还是直接编码组装?对于大多数需要在多处部署的应用程序来说,一个单独的配置文件会更合适。配置文件几乎都是XML 文件,XML 也的确很适合这一用途。不过,有些时候直接在程序代码中实现装配会更简单。譬如一个简单的应用程序,也没有很多部署上的变化,这时用几句代码来配置就比XML 文件要清晰得多。
与之相对的,有时应用程序的组装非常复杂,涉及大量的条件步骤。一旦编程语言中的配置逻辑开始变得复杂,你就应该用一种合适的语言来描述配置信息,使程序逻辑变得更清晰。然后,你可以编写一个构造器(builder)类来完成装配工作。如果使用构造器的情景不止一种,你可以提供多个构造器类,然后通过一个简单的配置文件在它们之间选择。
我常常发现,人们太急于定义配置文件。编程语言通常会提供简捷而强大的配置管理机制,现代编程语言也可以将程序编译成小的模块,并将其插入大型系统中。如果编译过程会很费力,脚本语言也可以在这方面提供帮助。
通常认为,配置文件不应该用编程语言来编写,因为它们需要能够被不懂编程的系统管理人员编辑。但是,这种情况出现的几率有多大呢?我们真的希望不懂编程的系统管理人员来改变一个复杂的服务器端应用程序的事务隔离等级吗?只有在非常简单的时候,非编程语言的配置文件才有最好的效果。如果配置信息开始变得复杂,就应该考虑选择一种合适的编程语言来编写配置文件。
”