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

这篇文章是我在patterns & practices看到的一篇有关EntLib5.0的文章,主要介绍了EntLib5.0的此次的架构变化由来,以为很不错,你们能够看一下!ios

 

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

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

背景

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 扩展。这不是朝正确的方向前进。ide

致力于简化

2009 年 4 月,咱们开始 Entlib 5.0 的开发。这一版本的主要目的是“以简化取胜”。这不只包括为最终用户(调用 Entlib 的开发人员)进行简化,也包括对 Entlib 代码自己进行简化。经过这些改进,咱们能够更方便地保持 Entlib 的进一步发展,客户也能够更方便地对它进行了解、自定义和扩展。函数

咱们知道,有些重要方面须要改进,其中之一是对象建立管道。保留两个并行但不一样的代码集实现同一功能会后患无穷。必须改变这种状况。性能

咱们制定了如下重构目标:

  • 现有客户端代码没必要仅因体系结构更改而更改。可要求从新编译,但不可要求更改源代码(固然,客户端 API 能够因其余 缘由进行更改)。可处理内部 API 或可扩展 API。
  • 删除冗余对象建立管道。应只保留一种(而非两种或更多)建立对象的方式。
  • 不使用 DI 的客户不该受在内部使用 DI 的 Entlib 的影响。
  • 确实须要 DI 的客户能够选择所需容器,而后从中获取本身的对象和 Entlib 对象。

不管从单独仍是组合的角度来说,这些目标都意味着要进行大量工做。从表面看,“一个对象建立管道”目标至关简单。咱们决定彻底删除基于 ObjectBuilder 的系统,在内部采用一个 DI 容器做为对象建立引擎。可是,咱们须要考虑“不该更改现有客户端代码”。传统 Entlib API 是一组静态外层和工厂。例如,使用日志记录块来记录一条消息可采用以下方式:

1
Logger.Write( "My Message" );

实际上,Logger 外层使用 LogWriter 对象的实例执行实际工做。那么,Logger 外层如何得到 LogWriter?LogWriter 是一个至关复杂的类,具备大量依赖关系,所以,若是采用新建的方式,配置是没法正确关联的。咱们认为,在 API 中,Logger 和全部其余静态类须要一个全局容器实例。咱们能够仅保留一个全局 Unity 容器,可是,咱们须要考虑“客户选择所需容器”。

咱们但愿 Unity 和 Entlib 组合能实现一流的体验。咱们也但愿经过其余容器也能实现这种一流体验。尽管 DI 容器的常规功能都一致,但访问这些功能的方式却有很大差别。实际上,许多容器建立者都认为他们的配置 API 是主要竞争优点。所以,咱们如何将 Entlib 配置映射到差别很大的容器 API 上?

传统计算机科学解决方案

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

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

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

图 1 TypeRegistration 类

public class TypeRegistration
    {
 
        public TypeRegistration(LambdaExpression expression);
        public TypeRegistration(LambdaExpression expression, Type serviceType);
 
        public Type ImplementationType { get; }
        public NewExpression NewExpressionBody { get; }
        public Type ServiceType { get; private set; }
        public string Name { get; set; }
 
        public static string DefaultName(Type serviceType);
        public static string DefaultName<TServiceType>();
 
        public LambdaExpression LambdaExpression { get; private set; }
 
         public bool IsDefault { get; set; }
 
         public TypeRegistrationLifetime Lifetime { get; set; }
 
         public IEnumerable<ParameterValue> ConstructorParameters { get; }
 
         public IEnumerable<InjectedProperty> InjectedProperties { get; }
    }

 

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

yield return new TypeRegistration<Database>(
       () => new SqlDatabase(
           ConnectionString,
           Container.Resolved<IDataInstrumentationProvider>(Name)))
       {
           Name = Name,
           Lifetime = TypeRegistrationLifetime.Transient
       };

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

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

public static T Resolved<T>(string name)
{
     return default(T);
}

 

为何要用它?请注意,此 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

相关文章
相关标签/搜索