这篇文章是我在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 的进一步发展,客户也能够更方便地对它进行了解、自定义和扩展。函数
咱们知道,有些重要方面须要改进,其中之一是对象建立管道。保留两个并行但不一样的代码集实现同一功能会后患无穷。必须改变这种状况。性能
咱们制定了如下重构目标:
不管从单独仍是组合的角度来说,这些目标都意味着要进行大量工做。从表面看,“一个对象建立管道”目标至关简单。咱们决定彻底删除基于 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 对象做为依赖关系进行注入,便可正常运行。
回顾一下咱们的目标以及咱们的设计是否符合这些目标。
现有客户端代码没必要仅因体系结构更改而更改。可要求从新编译,但不可要求更改源代码(固然,客户端 API 能够因其余 缘由进行更改)。可处理内部 API 或可扩展 API。
符合。原始 API 仍可正常运行。若是您不使用依赖关系注入,则不须要了解也不须要关心您的对象在底层是如何关联的。
删除冗余对象建立管道。应只保留一种(而非两种或更多)建立对象的方式。
符合。代码库再也不使用 ObjectBuilder 堆栈;如今,一切都经过 TypeRegistration 和配置程序机制进行构建。每一个容器都须要一个配置程序。
不使用 DI 的客户不该受在内部使用 DI 的 Entlib 的影响。
符合。DI 不会本身出现,除非您但愿它出现。
确实须要 DI 的客户能够选择所需容器,而后从中获取本身的对象和 Entlib 对象。
符合。您可直接使用所选 DI 容器,也可在静态外层以后使用它。
此外,咱们还实现了其余一些优势。简化了 Entlib 代码库。咱们从原始实现中删除了大约 200 个类。添加类型注册进行重构以后,一共减小了大约 80 个类。此外,添加的类比删除的类更简单,明显提升了总体结构的一致性,减小了移动部件或特殊状况。
另外一个优点是,重构的版本比原始版本更快一些,初步的非正式评估显示,性能提升了 10%。这些数字说明咱们的工做是有效的。原始代码中的复杂性大多源于针对 ObjectBuilder 的缓慢实现须要进行一系列性能优化。大多数 DI 容器针对其常规性能进行了大量工做。经过在容器之上重建 Entlib,能够利用这些性能优化工做,从而没必要本身完成大量这类工做。随着 Unity 和其余容器向前发展和优化,Entlib 的速度会更快,而无需咱们完成大量工做。
Enterprise Library 是一个很好的库示例,它真正利用依赖关系注入容器,而不会与这种容器紧密耦合。若是要编写使用 DI 容器的库,但不但愿将本身的选择强加给客户,能够借鉴咱们的设计思路。我认为,咱们针对“更改”设立的目标,尤为是最后两个,全部 库(而不只仅是 Entlib)做者都应将其考虑在内:
设计库时,须要考虑几个问题。请务必考虑如下问题:
在咱们的项目中,咱们总结出了一整套很好的答案。但愿咱们的示例能为您的设计提供帮助。
Chris Tavares 是 Microsoft 模式和实施方案组的开发人员,在该组中,他任 Enterprise Library 和 Unity 项目的开发主管。在 Microsoft 就任以前,他曾从事咨询、压缩包装软件和嵌入式系统的工做。他在博客中发表了 Entlib、p&p 和常规开发方面的文章,网址为:tavaresstudios.com。