在过去几年中,依赖关系注入 (DI) 模式在 .NET 开发人员社区一直受到关注。长时间以来,优秀的博客作者们讨论着 DI 的优点。MSDN 杂志 针对这一主题发表了多篇文章。.NET 4.0 将发布某种类似 DI 的功能,并计划以后将其发展为完整的 DI 系统。
阅读有关 DI 的博客文章时,我注意到,这一主题有一个很小却很重要的倾向。作者们谈论的是如何在整个应用程序环境中使用 DI。但如何编写使用 DI 的库或框架呢?关注重点的变化,对模式的使用有何影响?这是几个月前我们研究 Enterprise Library 5.0 的体系结构时首先遇到的问题。
Entlib 在很大程度上由配置驱动。它的大部分代码专用于读取配置,然后基于配置组合对象图。Entlib 对象可能非常复杂。大多数块都包含大量可选功能。此外,还有许多用于支持检测等功能的底层基础结构,它们也需要进行关联。我们不希望用户仅仅为了使用 Entlib 而去手动创建检测提供程序、读取配置,等等,所以将对象的创建封装在了工厂对象和静态外层之后。
Entlib 版本 2 到版本 4 的核心是一个名为“ObjectBuilder”的小型框架。ObjectBuilder 的作者将 ObjectBuilder 描述为“一种用于构建依赖关系注入容器的框架”。Enterprise Library 只是使用 ObjectBuilder 的 p&p 项目之一;其他使用 ObjectBuilder 的 p&p 项目包括 Composite UI Application Block、Smart Client Software Factory 和 Web Client Software Factory。Entlib 特别注重说明的“框架”部分,将一个很大的自定义功能集构建至 ObjectBuilder。读取 Entlib 配置和组合对象图时,需要使用这些自定义功能。在很多情况下,也需要用它们来改进现有 ObjectBuilder 实现的性能。
缺点在于,需要不少时间才能对 ObjectBuilder 本身(设计极为抽象,再加上完全没有文档,ObjectBuilder 的复杂性绝非虚言)和 Entlib 自定义功能都有所了解。因此,如果要编写与 Entlib 的对象创建策略有关的自定义块,一开始就需要进行大量学习,常常令人感觉困难重重。
此外,在 Entlib 4.0 中,我们发布了 Unity 依赖关系注入容器,这进一步增加了复杂性。DI 具有很多优点,我们希望确保为无法从众多优秀开放源代码容器中选用一种(无论什么原因)的客户提供一个很好的选择 — Microsoft 的 DI。当然,我们也希望在使用 Unity 时轻松实现 Entlib 对象的运行。在 Entlib 4.0 中,Unity 集成与现有 ObjectBuilder 基础结构一道,成为了并行对象创建系统。现在,块编写者不仅需要了解 ObjectBuilder 和 Entlib 扩展,还需要了解 Unity 内部机制,以及其中的部分 Entlib 扩展。这不是朝正确的方向前进。
2009 年 4 月,我们开始 Entlib 5.0 的开发。这一版本的主要目的是“以简化取胜”。这不仅包括为最终用户(调用 Entlib 的开发人员)进行简化,也包括对 Entlib 代码本身进行简化。通过这些改进,我们可以更方便地保持 Entlib 的进一步发展,客户也可以更方便地对它进行了解、自定义和扩展。
我们知道,有些重要方面需要改进,其中之一是对象创建管道。保留两个并行但不同的代码集实现同一功能会后患无穷。必须改变这种情况。
我们制定了以下重构目标:
现有客户端代码不必仅因体系结构更改而更改。可要求重新编译,但不可要求更改源代码(当然,客户端 API 可以因其他 原因进行更改)。可处理内部 API 或可扩展 API。
删除冗余对象创建管道。应只保留一种(而非两种或更多)创建对象的方式。
不使用 DI 的客户不应受在内部使用 DI 的 Entlib 的影响。
确实需要 DI 的客户可以选择所需容器,然后从中获取自己的对象和 Entlib 对象。
无论从单独还是组合的角度来讲,这些目标都意味着要进行大量工作。从表面看,“一个对象创建管道”目标相当简单。我们决定完全删除基于 ObjectBuilder 的系统,在内部采用一个 DI 容器作为对象创建引擎。但是,我们需要考虑“不应更改现有客户端代码”。传统 Entlib API 是一组静态外层和工厂。例如,使用日志记录块来记录一条消息可采用如下方式:
1
<code>Logger.Write(</code><code>"My Message"</code><code>);</code>
实际上,Logger 外层使用 LogWriter 对象的实例执行实际工作。那么,Logger 外层如何获得 LogWriter?LogWriter 是一个相当复杂的类,具有大量依赖关系,因此,如果采用新建的方式,配置是无法正确关联的。我们认为,在 API 中,Logger 和所有其他静态类需要一个全局容器实例。我们可以仅保留一个全局 Unity 容器,但是,我们需要考虑“客户选择所需容器”。
我们希望 Unity 和 Entlib 组合能实现一流的体验。我们也希望通过其他容器也能实现这种一流体验。尽管 DI 容器的常规功能都一致,但访问这些功能的方式却有很大差异。实际上,许多容器创建者都认为他们的配置 API 是主要竞争优势。因此,我们如何将 Entlib 配置映射到差异很大的容器 API 上?
这是计算机科学领域公认的事实:计算机科学中的所有问题都可以通过添加一个间接层解决。这正是我们解决容器独立问题的方法。我们把这个间接层称为容器配置程序。从本质上说,配置程序的作用是读取 Entlib 的配置,并对容器进行配置以便匹配。
遗憾的是,读取配置本身还不够。Entlib 的配置文件格式很大程度上是以最终用户为中心的。用户配置日志记录类别、异常策略和缓存后备存储。但不说明要完成相应功能实际所需的对象、要向构造函数传递的值以及要设置的属性。另一方面,DI 容器配置的内容则是“将此界面映射到此类型”、“调用此构造函数”和“设置此属性”等。我们需要另一个间接层将块的配置映射到实际所需对象来实现块。另一种方法是,让每一个配置程序(每个容器都需要一个配置程序)都知道每一个块的详细信息。很明显,这不可行;对块代码进行任何更改都将波及所有配置程序。如果有人编写自定义块,会发生什么情况?
我们最后开发了一组名为“TypeRegistration”的对象。各配置节负责生成一个类型注册模型 ,一系列 TypeRegistration 对象。TypeRegistration 的接口如图 1 所示。
图 1 TypeRegistration 类
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<code>public</code> <code>class</code> <code>TypeRegistration</code>
<code> </code><code>{</code>
<code> </code><code>public</code> <code>TypeRegistration(LambdaExpression expression);</code>
<code> </code><code>public</code> <code>TypeRegistration(LambdaExpression expression, Type serviceType);</code>
<code> </code><code>public</code> <code>Type ImplementationType {</code><code>get</code><code>; }</code>
<code> </code><code>public</code> <code>NewExpression NewExpressionBody {</code><code>get</code><code>; }</code>
<code> </code><code>public</code> <code>Type ServiceType {</code><code>get</code><code>;</code><code>private</code> <code>set</code><code>; }</code>
<code> </code><code>public</code> <code>string</code> <code>Name {</code><code>get</code><code>;</code><code>set</code><code>; }</code>
<code> </code><code>public</code> <code>static</code> <code>string</code> <code>DefaultName(Type serviceType);</code>
<code> </code><code>public</code> <code>static</code> <code>string</code> <code>DefaultName<TServiceType>();</code>
<code> </code><code>public</code> <code>LambdaExpression LambdaExpression {</code><code>get</code><code>;</code><code>private</code> <code>set</code><code>; }</code>
<code> </code><code>public</code> <code>bool</code> <code>IsDefault {</code><code>get</code><code>;</code><code>set</code><code>; }</code>
<code> </code><code>public</code> <code>TypeRegistrationLifetime Lifetime {</code><code>get</code><code>;</code><code>set</code><code>; }</code>
<code> </code><code>public</code> <code>IEnumerable<ParameterValue> ConstructorParameters {</code><code>get</code><code>; }</code>
<code> </code><code>public</code> <code>IEnumerable<InjectedProperty> InjectedProperties {</code><code>get</code><code>; }</code>
<code> </code><code>}</code>
该类的内容很多,但基本结构非常简单。该类描述单个类型所需的配置。ServiceType 是用户从容器进行请求的接口,而 ImplementationType 则是实际实现该接口的类型。Name 是注册服务时应使用的名称。生存期可确定单一实例(每次都返回同一实例)或瞬态(每次都创建新的实例)创建行为。其他在此就不一一列举了。我们选择使用 lambda 表达式来创建 TypeRegistration 对象,因为这样可以非常方便地在单一紧凑的范围内指定所有这些信息。以下是从数据访问块创建类型注册的示例:
<code>yield</code> <code>return</code> <code>new</code> <code>TypeRegistration<Database>(</code>
<code> </code><code>() =></code><code>new</code> <code>SqlDatabase(</code>
<code> </code><code>ConnectionString,</code>
<code> </code><code>Container.Resolved<IDataInstrumentationProvider>(Name)))</code>
<code> </code><code>{</code>
<code> </code><code>Name = Name,</code>
<code> </code><code>Lifetime = TypeRegistrationLifetime.Transient</code>
<code> </code><code>};</code>
此类型注册表示“如果请求名为 Name 的数据库,则返回一个新的 SqlDatabase 对象,该对象由 ConnectionString 和 IDataInstrumentationProvider 构造”。此处使用 lambda 的好处在于,在编写块时,可像直接新建对象一样构建这些表达式。编译器将对表达式进行类型检查,这样,我们就不会在无意中调用不存在的构造函数了。若要设置属性,可在 lambda 表达式内使用 C# 对象初始值设定项语法。TypeRegistration 类负责处理检查 lambda、提取构造函数签名、参数、类型等等的详细信息,以免配置程序作者为之操心。
我们用过的一个实用的技巧是调用“Container.Resolved”。该方法实际上不执行任何操作,它的实现如下:
<code>public</code> <code>static</code> <code>T Resolved<T>(</code><code>string</code> <code>name)</code>
<code>{</code>
<code> </code><code>return</code> <code>default</code><code>(T);</code>
<code>}</code>
为什么要用它?请注意,此 lambda 表达式实际上从不执行。相反,我们是在运行时通过运行表达式的结构提取注册信息。此方法只是一个众所周知的标记。如果将对 Container.Resolved 的调用作为参数,我们解释为“通过容器解析此参数”。我们发现,用表达式树执行高级工作时,此标记方法技术在很多情况下很有用。
最后,配置的容器的配置文件流程如图 2 所示。
图 2 容器配置
此处要说明一下我们的一项设计决策,这非常重要。TypeRegistration 系统现在不是(以后也绝不会成为)任何 DI 容器的通用、全面配置抽象概念。它是应 Enterprise Library 项目之需专门设计的。模式和实施方案组无意将它作为基于代码的指南。尽管基本概念(将配置提取到抽象模型中)普遍适用,此处的特定实现仅适用于 Entlib。
这样,我们就配置了容器。这只完成了一半工作。如何才能从容器中获取对象?在这方面,容器接口各不相同,令人欣慰的是,这种不同没有其配置接口那样大。
Enterprise Library 没有任何类型的引导需求。使用静态外层时,不需要在任何位置调用初始化函数。首次需要原始库时,可通过提取配置来运行它。我们必须复制此行为,以便在调用时,库已准备就绪可供使用。
我们需要的是众所周知的标准库,以便获取正确配置的容器。实际上,Common Service Locator 库具有以下功能之一:ServiceLocator.Current 静态属性。由于种种原因,我们决定不使用此属性。主要原因是,其他库,甚至应用程序本身都可使用 ServiceLocator.Current。我们需要在首次访问任何 Entlib 项目时,能够对容器进行设置;其他都不重要,比如人们试图弄明白为何其认真构建的容器会消失,或为何 Entlib 在首次调用可以运行,但后来就不行了。第二个原因与接口本身的一个缺陷有关。无法查询该属性,因而不能确定是否已对其进行了设置。这样就很难确定何时设置容器。
因此,我们构建了自己的静态属性:EnterpriseLibraryContainer.Current。在用户代码中也可以设置此属性,但它是 Enterprise Library 的特定部分,因此,减小了与其他库或主应用程序发生冲突的可能性。首次调用静态外层时,应检查 EnterpriseLibraryContainer.Current。如果已设置,则可使用其值。如果未设置,则应创建一个 UnityContainer 对象,用配置程序对其进行配置,并将其设置为 Current 属性的值。
这样,现在就有了三种不同的方式,可访问 Enterprise Library 的功能。如果使用传统 API,一切都会正常运行。在底层,将创建和使用 Unity 容器。如果要在应用程序中使用不同的 DI 容器,不希望进程中有 Unity,但仍使用传统 API,则可以使用配置程序来配置您的容器,将其封装在 IServiceLocator 中,并附于 EnterpriseLibraryContainer.Current 中,这样,外层仍将正常运行。它们现在才在底层使用您所选择的容器。实际上,在主 Entlib 项目中,我们不提供任何容器配置程序(Unity 除外);我们希望,社区将为其他容器实现配置程序。
第二种方法是直接使用 EnterpriseLibraryContainer.Current。可调用 GetInstance<T>() 以获取任何 Enterprise Library 对象,该对象会提供一个配置程序。同样,如果愿意,也可在其后附一个其他容器。
最后一种方法,您可以直接使用所选容器。必须使用配置程序将 Entlib 配置引导到容器中,但如果要使用容器,则需要对其进行设置,这并不是一个新要求。然后,将所需 Entlib 对象作为依赖关系进行注入,即可正常运行。
回顾一下我们的目标以及我们的设计是否符合这些目标。
符合。原始 API 仍可正常运行。如果您不使用依赖关系注入,则不需要了解也不需要关心您的对象在底层是如何关联的。
符合。代码库不再使用 ObjectBuilder 堆栈;现在,一切都通过 TypeRegistration 和配置程序机制进行构建。每个容器都需要一个配置程序。
符合。DI 不会自己出现,除非您希望它出现。
符合。您可直接使用所选 DI 容器,也可在静态外层之后使用它。
此外,我们还实现了其他一些优点。简化了 Entlib 代码库。我们从原始实现中删除了大约 200 个类。添加类型注册进行重构之后,一共减少了大约 80 个类。此外,添加的类比删除的类更简单,明显提高了整体结构的一致性,减少了移动部件或特殊情况。
另一个优势是,重构的版本比原始版本更快一些,初步的非正式评估显示,性能提高了 10%。这些数字说明我们的工作是有效的。原始代码中的复杂性大多源于针对 ObjectBuilder 的缓慢实现需要进行一系列性能优化。大多数 DI 容器针对其常规性能进行了大量工作。通过在容器之上重建 Entlib,可以利用这些性能优化工作,从而不必自己完成大量这类工作。随着 Unity 和其他容器向前发展和优化,Entlib 的速度会更快,而无需我们完成大量工作。
Enterprise Library 是一个很好的库示例,它真正利用依赖关系注入容器,而不会与这种容器紧密耦合。如果要编写使用 DI 容器的库,但不希望将自己的选择强加给客户,可以借鉴我们的设计思路。我认为,我们针对“更改”设立的目标,尤其是最后两个,所有 库(而不仅仅是 Entlib)作者都应将其考虑在内:
设计库时,需要考虑几个问题。请务必考虑以下问题:
库采用什么引导方式?客户是否必须完成特定工作才能设置您的代码,或者,您是否有可正常运行的静态入口点?
您如何对对象图进行建模,以便在配置容器时无需将调用硬编码到该容器中?请参考我们的 TypeRegistration 系统,寻找解决方法。
如何管理要使用的容器?是在内部处理,还是由调用方进行管理?调用方如何通知您要使用哪个容器?
在我们的项目中,我们总结出了一整套很好的答案。希望我们的示例能为您的设计提供帮助。
微软企业库5.0 学习之路系列文章索引:
<a href="http://www.cnblogs.com/kyo-yo/archive/2010/06/07/Study-Entlib5-First.html">第一步、基本入门</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/06/10/Learning-EntLib-Second.html">第二步、使用VS2010+Data Access模块建立多数据库项目</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/06/24/Learning-EntLib-Forth-Use-Caching.html">第四步、使用缓存提高网站的性能(EntLib Caching)</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/07/19/Learning-EntLib-Fifth-Introduction-Validation-module-information-Part1.html">第五步、介绍EntLib.Validation模块信息、验证器的实现层级及内置的各种验证器的使用方法——上篇</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/08/02/Learning-EntLib-Fifth-Introduction-Validation-module-information-Part3.html">第五步、介绍EntLib.Validation模块信息、验证器的实现层级及内置的各种验证器的使用方法——下篇</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/08/04/Learning-EntLib-Sixth-Use-Validation-To-Server-Validate.html">第六步、使用Validation模块进行服务器端数据验证</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/08/09/Learning-EntLib-Seventh-Introduce-Cryptographer-and-Expand.html">第七步、Cryptographer加密模块简单分析、自定义加密接口及使用—上篇</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/08/11/Learning-EntLib-Seventh-Introduce-Cryptographer-and-Expand-Part2.html">第七步、Cryptographer加密模块简单分析、自定义加密接口及使用—下篇</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/08/16/Learning-EntLib-Eighth-Use-Configuration-Setting-To-Manage-ConfigInfo.html">第八步、使用Configuration Setting模块等多种方式分类管理企业库配置信息</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/09/01/Learning-EntLib-Ninth-Use-PolicyInjection-Module-AOP-PART1-Basic-Use-Of-Dscription.html">第九步、使用PolicyInjection模块进行AOP—PART1——基本使用介绍</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/09/20/Learning-EntLib-Ninth-Use-PolicyInjection-Module-AOP-PART2-Custom-Matching-Rule.html">第九步、使用PolicyInjection模块进行AOP—PART2——自定义Matching Rule</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/10/08/Learning-EntLib-Ninth-Use-PolicyInjection-Module-AOP-PART3-Built-in-Call-Handler-Description.html">第九步、使用PolicyInjection模块进行AOP—PART3——内置Call Handler介绍</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/11/01/Learning-EntLib-Tenth-Decoupling-Your-System-Using-The-Unity-PART1-Why-Use-Unity.html">第十步、使用Unity解耦你的系统—PART1——为什么要使用Unity?</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/11/08/Learning-EntLib-Tenth-Decoupling-Your-System-Using-The-Unity-PART2-Learn-To-Use-Unity-One.html">第十步、使用Unity解耦你的系统—PART2——了解Unity的使用方法(1)</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/11/10/Learning-EntLib-Tenth-Decoupling-Your-System-Using-The-Unity-PART2-Learn-To-Use-Unity-Two.html">第十步、使用Unity解耦你的系统—PART2——了解Unity的使用方法(2)</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/11/22/Learning-EntLib-Tenth-Decoupling-Your-System-Using-The-Unity-PART2-Learn-To-Use-Unity-Three.html">第十步、使用Unity解耦你的系统—PART2——了解Unity的使用方法(3)</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/11/29/Learning-EntLib-Tenth-Decoupling-Your-System-Using-The-Unity-PART3-Dependency-Injection.html">第十步、使用Unity解耦你的系统—PART3——依赖注入</a> <a href="http://www.cnblogs.com/kyo-yo/archive/2010/12/08/Learning-EntLib-Tenth-Decoupling-Your-System-Using-The-Unity-PART3-Unity-And-PIAB.html">第十步、使用Unity解耦你的系统—PART4——Unity&PIAB</a> 扩展学习: <a href="http://www.cnblogs.com/kyo-yo/archive/2010/09/16/Learning-EntLib-Extended-Learning-Articles-Library-Dependency-Injection.html">扩展学习篇、库中的依赖关系注入(重构 Microsoft Enterprise Library)[转]</a> 本文转自kyo-yo博客园博客,原文链接:http://www.cnblogs.com/kyo-yo/archive/2010/09/16/Learning-EntLib-Extended-Learning-Articles-Library-Dependency-Injection.html,如需转载请自行联系原作者