一切福田,不離方寸,從心而覓,感無不通。

[EntLib]微软企业库5.0 学习之路——扩展学习篇、库中的依赖关系注入(重构 Microsoft Enterprise Library)

在过去几年中,依赖关系注入 (DI) 模式在 .NET 开发人员社区一直受到关注。长时间以来,优秀的博客作者们讨论着 DI 的优点。MSDN 杂志 针对这一主题发表了多篇文章。.NET 4.0 将发布某种类似 DI 的功能,并计划以后将其发展为完整的 DI 系统。

阅读有关 DI 的博客文章时,我注意到,这一主题有一个很小却很重要的倾向。作者们谈论的是如何在整个应用程序环境中使用 DI。但如何编写使用 DI 的库或框架呢?关注重点的变化,对模式的使用有何影响?这是几个月前我们研究 Enterprise Library 5.0 的体系结构时首先遇到的问题。

背景

Microsoft Enterprise Library (Entlib) 是 Microsoft 模式与实施方案组开发的著名版本。迄今为止,其下载次数已超过两百万。可以想到的单位 — 从金融机构、政府机关到餐厅和医疗设备制造商 — 都在使用它。顾名思义,Entlib 是一种库,可帮助开发人员处理许多企业开发人员都会面临的问题。如果您不熟悉 Entlib,请访问我们的网站 p&p 开发中心,以了解更多信息。

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 是一组静态外层和工厂。例如,使用日志记录块来记录一条消息可采用如下方式:

实际上,Logger 外层使用 LogWriter 对象的实例执行实际工作。那么,Logger 外层如何获得 LogWriter?LogWriter 是一个相当复杂的类,具有大量依赖关系,因此,如果采用新建的方式,配置是无法正确关联的。我们认为,在 API 中,Logger 和所有其他静态类需要一个全局容器实例。我们可以仅保留一个全局 Unity 容器,但是,我们需要考虑“客户选择所需容器”。

我们希望 Unity 和 Entlib 组合能实现一流的体验。我们也希望通过其他容器也能实现这种一流体验。尽管 DI 容器的常规功能都一致,但访问这些功能的方式却有很大差异。实际上,许多容器创建者都认为他们的配置 API 是主要竞争优势。因此,我们如何将 Entlib 配置映射到差异很大的容器 API 上?

传统计算机科学解决方案

这是计算机科学领域公认的事实:计算机科学中的所有问题都可以通过添加一个间接层解决。这正是我们解决容器独立问题的方法。我们把这个间接层称为容器配置程序。从本质上说,配置程序的作用是读取 Entlib 的配置,并对容器进行配置以便匹配。

遗憾的是,读取配置本身还不够。Entlib 的配置文件格式很大程度上是以最终用户为中心的。用户配置日志记录类别、异常策略和缓存后备存储。但不说明要完成相应功能实际所需的对象、要向构造函数传递的值以及要设置的属性。另一方面,DI 容器配置的内容则是“将此界面映射到此类型”、“调用此构造函数”和“设置此属性”等。我们需要另一个间接层将块的配置映射到实际所需对象来实现块。另一种方法是,让每一个配置程序(每个容器都需要一个配置程序)都知道每一个块的详细信息。很明显,这不可行;对块代码进行任何更改都将波及所有配置程序。如果有人编写自定义块,会发生什么情况?

我们最后开发了一组名为“TypeRegistration”的对象。各配置节负责生成一个类型注册模型 ,一系列 TypeRegistration 对象。TypeRegistration 的接口如图 1 所示。

图 1 TypeRegistration 类

该类的内容很多,但基本结构非常简单。该类描述单个类型所需的配置。ServiceType 是用户从容器进行请求的接口,而 ImplementationType 则是实际实现该接口的类型。Name 是注册服务时应使用的名称。生存期可确定单一实例(每次都返回同一实例)或瞬态(每次都创建新的实例)创建行为。其他在此就不一一列举了。我们选择使用 lambda 表达式来创建 TypeRegistration 对象,因为这样可以非常方便地在单一紧凑的范围内指定所有这些信息。以下是从数据访问块创建类型注册的示例:

此类型注册表示“如果请求名为 Name 的数据库,则返回一个新的 SqlDatabase 对象,该对象由 ConnectionString 和 IDataInstrumentationProvider 构造”。此处使用 lambda 的好处在于,在编写块时,可像直接新建对象一样构建这些表达式。编译器将对表达式进行类型检查,这样,我们就不会在无意中调用不存在的构造函数了。若要设置属性,可在 lambda 表达式内使用 C# 对象初始值设定项语法。TypeRegistration 类负责处理检查 lambda、提取构造函数签名、参数、类型等等的详细信息,以免配置程序作者为之操心。

我们用过的一个实用的技巧是调用“Container.Resolved”。该方法实际上不执行任何操作,它的实现如下:

为什么要用它?请注意,此 lambda 表达式实际上从不执行。相反,我们是在运行时通过运行表达式的结构提取注册信息。此方法只是一个众所周知的标记。如果将对 Container.Resolved 的调用作为参数,我们解释为“通过容器解析此参数”。我们发现,用表达式树执行高级工作时,此标记方法技术在很多情况下很有用。

最后,配置的容器的配置文件流程如图 2 所示。


图 2 容器配置

此处要说明一下我们的一项设计决策,这非常重要。TypeRegistration 系统现在不是(以后也绝不会成为)任何 DI 容器的通用、全面配置抽象概念。它是应 Enterprise Library 项目之需专门设计的。模式和实施方案组无意将它作为基于代码的指南。尽管基本概念(将配置提取到抽象模型中)普遍适用,此处的特定实现仅适用于 Entlib。

从容器中获取对象

这样,我们就配置了容器。这只完成了一半工作。如何才能从容器中获取对象?在这方面,容器接口各不相同,令人欣慰的是,这种不同没有其配置接口那样大。

很幸运,这时我们不必创造新的抽象概念。受 2008 年夏 Jeremy Miller 发表的博客文章的启发,Microsoft 的模式和实施方案组、MEF 团队和许多不同的 DI 容器的作者们合作,定义了一个最低通用标准,以解决从容器中解析出对象的问题。该标准作为 Common Service Locator 项目发布在 Codeplex 和 MSDN 上。该接口正好满足我们的需要;在 Enterprise Library 中,无论何时需要从容器中获取对象,都可通过该接口进行调用,并与所用的特定容器隔离开。当然,下一个问题是:容器在哪里?

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 对象作为依赖关系进行注入,即可正常运行。

如何评价我们的工作?

回顾一下我们的目标以及我们的设计是否符合这些目标。

  1. 现有客户端代码不必仅因体系结构更改而更改。可要求重新编译,但不可要求更改源代码(当然,客户端 API 可以因其他 原因进行更改)。可处理内部 API 或可扩展 API。

    符合。原始 API 仍可正常运行。如果您不使用依赖关系注入,则不需要了解也不需要关心您的对象在底层是如何关联的。

  2. 删除冗余对象创建管道。应只保留一种(而非两种或更多)创建对象的方式。

    符合。代码库不再使用 ObjectBuilder 堆栈;现在,一切都通过 TypeRegistration 和配置程序机制进行构建。每个容器都需要一个配置程序。

  3. 不使用 DI 的客户不应受在内部使用 DI 的 Entlib 的影响。

    符合。DI 不会自己出现,除非您希望它出现。

  4. 确实需要 DI 的客户可以选择所需容器,然后从中获取自己的对象和 Entlib 对象。

    符合。您可直接使用所选 DI 容器,也可在静态外层之后使用它。

此外,我们还实现了其他一些优点。简化了 Entlib 代码库。我们从原始实现中删除了大约 200 个类。添加类型注册进行重构之后,一共减少了大约 80 个类。此外,添加的类比删除的类更简单,明显提高了整体结构的一致性,减少了移动部件或特殊情况。

另一个优势是,重构的版本比原始版本更快一些,初步的非正式评估显示,性能提高了 10%。这些数字说明我们的工作是有效的。原始代码中的复杂性大多源于针对 ObjectBuilder 的缓慢实现需要进行一系列性能优化。大多数 DI 容器针对其常规性能进行了大量工作。通过在容器之上重建 Entlib,可以利用这些性能优化工作,从而不必自己完成大量这类工作。随着 Unity 和其他容器向前发展和优化,Entlib 的速度会更快,而无需我们完成大量工作。

可供其他库借鉴的经验

Enterprise Library 是一个很好的库示例,它真正利用依赖关系注入容器,而不会与这种容器紧密耦合。如果要编写使用 DI 容器的库,但不希望将自己的选择强加给客户,可以借鉴我们的设计思路。我认为,我们针对“更改”设立的目标,尤其是最后两个,所有 库(而不仅仅是 Entlib)作者都应将其考虑在内:

  • 不使用 DI 的客户不应受在内部使用 DI 的 Entlib 的影响。

  • 确实需要 DI 的客户可以选择所需容器,然后从中获取自己的对象和 Entlib 对象。

设计库时,需要考虑几个问题。请务必考虑以下问题:

  • 库采用什么引导方式?客户是否必须完成特定工作才能设置您的代码,或者,您是否有可正常运行的静态入口点?

  • 您如何对对象图进行建模,以便在配置容器时无需将调用硬编码到该容器中?请参考我们的 TypeRegistration 系统,寻找解决方法。

  • 如何管理要使用的容器?是在内部处理,还是由调用方进行管理?调用方如何通知您要使用哪个容器?

在我们的项目中,我们总结出了一整套很好的答案。希望我们的示例能为您的设计提供帮助。


Chris Tavares 是 Microsoft 模式和实施方案组的开发人员,在该组中,他任 Enterprise Library 和 Unity 项目的开发主管。在 Microsoft 就职之前,他曾从事咨询、压缩包装软件和嵌入式系统的工作。他在博客中发表了 Entlib、p&p 和常规开发方面的文章,网址为:tavaresstudios.com

转载自:http://msdn.microsoft.com/zh-cn/magazine/ee335709.aspx