通过Unity依赖注入

时间:2023-03-08 16:50:36

前言

Unity容器的思想起始于我在为Web Client Sofitware Factory项目工作的时候,微软的patterns&practices团队已经使用依赖注入的概念好几年了在那时候,最著名的是Composite Application Block(CAB)。它也是Enterprise Library 2.0的核心配置,当我们开始为web装备组件应用程序的时候它又一次成为了中心(一个以CWAB闻名的库)。

我们的目标一直是促进依赖注入的概念做为一个建立松散耦合系统的方式。然而,那时候p&p实现依赖注入的方式和我们现在考虑的已经不一样了。替代一个单独的重用容器,DI实现应该是一个专门的系统以使用。我们使用一个叫做ObjectBuilder的库,它被描述为“一个建立DI容器的框架”。理论上,让我们每个项目写一个我们确实需要的容器。

一个崇高的抱负,但是在实践中它工作的不是很好。ObjectBuilder是一个高度解耦、抽象的部分集合并且不得不手动集成。和一个缺少文档的文件组合它将花费大量时间理解什么要去哪和怎么把它放在一起形成一个有用组合。时间将花费在写代码,调试和优化DI容器而不是我们实际在项目中需要的。

它甚至有更多的乐趣当某个人要使用CAB(在一个ObjectBuilder版本基础上使用一个DI容器)和Enterprise Library(在不同ObjectBduilder版本的基础上使用分离的容器)在一个项目中。集成是很困难的;仅仅在同一个项目中处理两个不同版本的ObjectBuilder引用已经是一个挑战了。还要把一次性容器导向一次性扩张和集成接口:在Enterprise Library中工作的在CAB无用,反之亦然。

紧要关头来了,当我们又花费了一周在Web Client Software Factory项目快结束的时候修复一堆在CWAB里的bugs:这些bugs很像我们之前在CAB中修复过的。它不可以再好一点吗,我们提出问题,我们是否可以只有一个容器实现并且用它替代一遍又一遍的写容器?

在这个基础下构建Unity。Enterprise Library 4.0团队把依赖注入Application Block(开始的时候,作为Unity使用而被大家认知)放入产品订单中。我们为了项目的目标是明确的。首先,介绍和推销依赖注入的概念给我们的社区,没有太多底层实现细节的阻碍。其次,提供一个核心容器和简单使用的API那样我们,微软的其它团队,或者谁使用开源项目不好用的都可以使用。第三,有一个多样性扩张机制那么新的特性可以被任何人添加进来而不需要拆开核心代码。

在我看来Unity因为这些目标而获得成功。我特别自豪我们如何影响了.NET开发者社区。Unity在.NET生态系统中快速变成了一个最常用的DI容器。更重要的,其它的DI容器使用也增加了。Unity介绍DI给一些从来没有听说过DI的人群。这些人中的一些人不久就转移到了他们需要的其它容器。那不是Unity的损失:这些人使用Unity的概念,那是重要的部分。

没有更多的关于DI容器的福音可以公布了。在我看来,这是因为DI不再是一个“专家技术”:它现在是主流的一部分。当微软(特别是ASP.NET MVC和WebAPI)来的framworks支持DI内建的时候,你知道一个理念已经到达了核心听众。我认为Unity在这个事件中起到了很大的作用。

我很兴奋可以看到这本书发布。第一次,有一个地方你可以同时查找DI的概念和怎么使用Unity容器应用这些概念,并且有一个扩张故事的覆盖率,有一些事情我一直要写但是没有开始。我再也不用感到愧疚了。

读这本书,拥抱概念,享受松散耦合,高内聚软件,DI使构建这一切如此简单!

Chris Tavares

Redmond,WA,USA

April 2013


引语

关于这个指南

这个指南是一个资源可用于Unity3版本帮助你学习Unity,学习一些关于Unity问题和议题帮助你在你的应用程序中拿出和开始Unity。Unity是主要的依赖注入容器因此指南也包含一个依赖注入的介绍,即便你不计划使用Unity你也可以孤立的阅读,虽然我们希望你使用它。

章节的设计是顺序阅读,每一个章节成立在上一个章节之上,交互的章节覆盖了一些概念背景材料,这些章节指出在你自己的应用程序中使用Unity。假如你已经熟悉依赖注入和拦截的概念,你或许可以关注第三章,“通过Unity使用依赖注入”,第五章,“通过Unity拦截”,和第六章,“Unity扩展”。

前两章介绍了概念背景和解释了依赖注入是什么,它的优点和缺点是什么,当你考虑使用它的时候。章节三然后应用这些理论性的知识,在各种各样的场景怎么使用Unity容器并提供实例和引导。

第四章描述拦截做为一个技术动态插入代码为你的应用程序提供横切支持。

第五章讨论关于interception and policy injection(拦截和策略注入)的高级主题,独自面对选择,拿出一些建议当你使用它的时候。

剩下的章节介绍一些方法,你可以扩展Unity,比如创建容器扩展或者创建自定义声明周期管理者。

这个指南同时包括几个论文,叫做Tales from the Trenches,开发者适应和自定义Unity。额外的论文可能可以从网上(http://msdn.com/unity)得到,所以确保检查他们出来。假如你要和开发者社区详尽的分享你的故事,把它发送到ourstory@microsoft.com

所有的章节包括引用附加的资源,比如书,博客,和论文将会提供额外的细节假如你想要对一些主题进行更深入的探索。为了你的方便,有一个在线文献目录包些所有的链接所以这些资源只要点击就能阅读。你可以在http://aka.ms/unitybiblio  找到文献目录。

在这些章节中的大多实例代码来自一个应用程序集合你可以从http://unity.codeplex.com/downloads/get/683531 下载。

这个指南不包括Unity特征的每一个细节信息或者在Unity程序集中的每一个类。要查找信息,你应该查看https://unity.codeplex.com/downloads/get/669364 和  https://msdn.microsoft.com/en-us/library/dn170424(v=pandp.30).aspx

这本书是为谁写的

这本书打算给一些设计师,开发者,或者设计,建立,以及操作应用程序和服务的信息技术(IT)专业者和想要学习怎么使用Unity容器依赖注入容器给他的应用程序带来好处。你应该熟悉微软的.NET Framework,和微软的Visual Studio才能得到从这个指南里得到所有的好处。

1  介绍

在你学习关于依赖注入和Unity之前,你需要理解为什么你应该使用它们。并且为了理解为什么你应该使用它们,你应该了解依赖注入和Unity设计是为了解决什么问题。这个介绍章节不会说太多关于Unity,或者确实讲太多关于依赖注入,但是它将提供一些需要的背景信息将帮助你领会依赖注入作为一个技术带来的益处并且为什么Unity为什么那么做。

下一个章节,第二章,“依赖注入”,将会教你依赖注入如何帮助你符合这一章要求的概述,接下来的章节,第三章,“通过Unity实现历来注入”,说明Unity在你的应用程序中如何帮助你实现依赖注入的方法。

动机

当你设计和开发软件系统,有很多需求要考虑。一些将会指定为系统问题而一些是更常见的目的。你可以把一些需求归类为功能需求,一些做为非功能需求(或者质量属性)。全面的功能集合将会改变对应不同的系统。以下的需求集合概述是公共需求,特别是对于行业(LOB)软件系统带有相对较长的生命周期。没有必要把你开发的每一个系统都看得很重要,但是你可以确信一些系统将会在你工作的众多的项目的需求列表上。

可维护性

随着系统越来越大,预期的系统生命周期越来越长,维护这些系统变得越来越有挑战。经常,起始团队成员开发的系统不再可用,或者不记得系统的详情。文档可能已经过期甚至丢失了。同时,商务可能要求转换动作以符合一些有压力的商务需求。可维护性是一个软件系统的质量决定了当你更新系统的时有多容易和多有效。你可能需要更新一个系统假如发现了一个缺陷需要修复(换句话说,执行维护保养),假如一些在操作环境的改变需要你对系统进行改变,或者假如你需要给系统添加一个新的产品特点以符合商业需求(完成维护保养)。可维护的系统提升组织的灵活性并减少开销。因此,做为你的设计目标你应该包括可维护性,和其他的比如可靠性、安全性和可扩展性。

可测试性

一个可测试的系统是指,你可以有效的测试系统单独的部分。设计和书写高效的测试用例和设计和书写可测试的应用程序代码一样是个挑战,特别随着系统变得越来越大和越来越复杂。方法论比如测试驱动开发(TDD)要求你在写任何代码实现一个新的特性之前先写单元测试,以此为目标设计技术会改善你的应用程序质量。这种设计技术同时帮助你扩大单元测试的范围,减少回溯的可能性,并且使重构变的更容易。然而,作为你的测试操作的一部分你也应该包含其它类型的测试,比如可接受性测试,集成测试,和压力测试。

运行测试会花费金钱和消耗时间因为要求在一个真实的环境测试。例如,为了一些云端的应用程序的测试,你需要把应用程序部署到云环境和在云上运行测试。假如你使用TDD,在云端一直运行所有测试可能不切实际因为部署你的应用程序时间比不是在本地模拟器上的时间花的更多。在这类情况下,为了使你能隔离的运行配套的单元测试你可能决定使用双测试(简单stubs或者可检验mocks)替代真实组件在云端实现测试在标准的TDD开发中。

测试性应该和可维护性和灵敏度一起作为你的系统的设计目标,一个可测试系统一般是可维护的,反之亦然。

灵活性和扩展性

灵活性和扩展性也是常常是企业应用程序清单上的理想属性。给出的商业需求经常发生变更,即使在部署应用程序期间和已经发布成产品,你应该尝试设计足够灵活的应用程序以至于它可以在不同的途径下可以适应工作和扩展,你可以添加新的功能。例如,你可能需要把你的应用程序从本地部署装换为云端部署。

后期绑定

在一些应用程序场景下,你可能有一个支持后期绑定的需求。后期绑定很有用假如你需要不重新编译替换你的系统的一部分的能力。例如,你的应用程序支持多个关系数据库,每个支持的数据库类型带有一个分离的模块。你可以使用一个声明配置告诉应用程序在运行时使用一个指定的模块。后期绑定其它有用的场景是使系统用户通过插件提供他们自己的定制化服务。此外,你可以指挥系统通过一个配置集合或者一个约定(系统在文件系统中扫描特别的位置以供模块使用)使用指定的定制化服务。

平行开发

当你在开发大规模(或者甚至小型和中型规模)系统,全部开发团队同时开发一个功能和组件是不现实的。事实上,你将分配不同的功能和组件给小组让他们平行开发。即使这种方法使你减少了在项目上的期间,它也带来了额外的复杂度:你需要管理多个组并确保你可以集成不同组开发的应用程序使之正确工作。

横切关注点

企业应用程序典型的需要处理一系列的横切关注点比如验证,异常处理,和登录。你可能在应用程序的很多不同区域需要这些功能并且你将要以标准化、一致的方法实现它们以提高系统的可维护性。观念上,你要一个机制使你有效的和透明的添加行为到你的对象要么在设计时要么在运行时而不需要你对已存在的类型做任何更改。通常,你需要在运行时配置这些功能的能力并在一些情况下,在存在的应用程序中添加功能放值一个新的横切关注点。

松散耦合

你可以查看前面章节列出的很多需求确保你的设计引起应用程序松散耦合,多个部分组成了应用程序。松散耦合,对立于紧耦合,意味着减少组成你的系统的组件之间依赖,因为系统的各个部分大部分独立于其它。

一个简单的实例

以下的实例阐明了紧耦合,ManagementController类型直接依赖TenantStore类型。这些类可能不同于Visual Studio项目。

public class TenantStore
{
...
public Tenant GetTenant(string tenant){
...
} public IEumerable<string> GetTenantNames(){
...
}
} public class ManagementController{ private readonly TenantStore tenantStore;
public ManagementController(){
tenantStore=new TenantStore(...);
} public ActionResult Index(){
var model=new TenantPageViewData<IEnumerable<string>>(this.tenantStore.GetTenantNames()){
Title="Subscribers"
};
return this.View(model);
} public ActionResult Detail(string tenant){
var contentModel=this.tenantStore.GetTenant(tenant);
var model=new TenantPageViewData<Tenant>(contentModel){
Title=string.format("{0}details",contentMdoel.Name)
};
return this.View(model); }
......
}

ManagementController和TenantStore通过这个导入被用于各种各样的表单。即使ManagementController是一个ASP.NET MVC控制器,你不需要知道遵从MVC。然而,这些实例意在看起来像你将在真实世界中见到的各种类型一样的,特别是在第三章的实例。

在这个例子中,TenantStore类实现了一个仓储,处理访问底层数据存储例如一个关系数据库,ManagementController是一个MVC控制器类从仓储请求数据。注意ManagementController类必须要么实例化一个TanantStore对象要么从什么地方获取一个TanentStore对象的引用,在它可以调用GetTenant和GetTenantNames方法之前。ManagementController类依赖于指定的、具体的TenantStore类型。

假如你为企业应用程序重新提及在这章开始时的公共理想需求,你可以评估在之前代码示例轮廓对你有多大帮助。

即便这个简单的示例显示只有单一客户端TenantStore类,实际上在你的应用程序中可能有多个客户端类使用TenantStore类。如果你设想每个客户端类有一个实例负责或者在运行时定位一个TenantStore类型,那么所有的都需要改变当TenantStore类的实现改变了。这样一来维护TenantStore类可能会更复杂,更容易出错,并且消耗更多时间。

为了ManagementController类中的Index和Detail方法运行单元测试,你需要实例化一个TenantStore对象并且确保底层数据存储包含合适的测试数据以测试。这使得测试过程变得复杂,并且依赖于你使用的数据存储,运行测试可能消耗更多时间因为你必须创建和在数据存储中填入正确数据。它也使测试变得更脆弱。

使用不同的数据存储有可能改变TenantStore类的实现,例如Windows Azure表存储替代SQL Server。不管怎么样,对于使用TenantStore实例的客户端类它可能需要一些改变如果它必须为这些客户端类提供一些初始化数据,比如连接字符串。

这个方法不能使用晚期绑定因为客户端类直接编译TenantStore类使用。

假如你需要为横切关注点添加支持比如登录到多个存储类,包括TenantStore类,你需要独立的修改和配置每个你的存储类。

以下代码示例显示了一个小的更改,在客户端里的ManagementController类的构造器现在接收到一个实现了ITenantStore接口的对象并且TenantStore类提供了一个相同接口的实现。

public interface ITenantStore
{
void Initialize();
Tenant GetTenant(string tenant);
IEnumerable<string> GetTenantNames();
void SaveTenant(Tenant tenant);
void UploadLogo(string tenant, byte[] logo);
} public class TenantStore : ITenantStore
{
...
public TenantStore()
{
....
}
...
} public class ManagementController : Controller
{
private readonly ITenantStore tenantStore; public ManagementController(ITenantStore tenantStore)
{
this.tenantStore = tenantStore;
} public ActionResult Index()
{
...
} public ActionResult Detail(string tenant)
{
...
}
...
}

这个改变直接影响了你可以有多简单的符合需求清单。

现在ManagementController类很干净,并且任何其他的客户端TenantStore类不再为TenantStore实例对象负责,尽管示例代码已经显示不再为某个类或组件实例化负责。从维护的观点看,责任属于一个类好过多个类。

现在很清楚controller依赖什么是从构造器参数来的代替埋葬在控制器方法实现内部。

为了测试一些客户端类的行为比如ManagementController类,你现在可以提供一个轻量的ITenantStore接口的实现返回一些样本数据。这替代了创建TenantStore对象查询底层数据存储取出样本数据。

引入ITenantStore接口使得它更容易替换存储实现而不需要更改客户端类因为它们所期待的是一个实现接口的对象。假如接口是在一个分离的项目实现,那么包含客户端类的项目只需要持有一个包含接口定义的项目的引用。

负责实例化存储的类现在也可能为应用程序提供一个额外的服务。它可以控制它创建的ITenantStore实例的生命周期,例如每次创建一个新的对象ManagementController客户端类需要一个实例,或者维护一个单独的实例,当一个客户端需要它的时候传递它的引用。

现在有可能使用晚期绑定了因为客户端类仅仅引用了ITenantStore接口类型。应用程序可以在运行时创建一个实现接口的对象,也许在一个配置集合的基础上,并把对象传递给客户端类。例如,应用程序可能创建配置文件,并且把配置文件传递给ManagementController类的构造器。

假如接口定义是通过协议的,两个团队可以并行工作在存储类和控制器类上。

负责创建存储类实例的类现在可以为横切关注点添加支持在传递存储实例给客户端之前,例如使用装饰模式传递一个实现了关注横切点的对象。你不需要改变客户端类或存储类添加对横切关注点的支持例如登录或者异常处理。

在第二段代码示例显示的途径是一个松散耦合设计使用了接口。假如我们可以在类之间移除一个直接依赖,它减少解决方案耦合的层次并且帮助增加可维护性,可测试性,灵活性和扩展性。

在第二段代码示例没有显示的是如何依赖注入并且Unity容器适应到图片,即使你可能猜到它们将会负责创建示例并传递给客户端类。第二章描述依赖注入的角色做为一个技术以支持松散耦合,并且第三章描述Unity如何帮助你在你的应用程序中实现依赖注入。

什么时候你应该使用一个松散耦合设计?

在我们进一步讨论依赖注入和Unity之前,你应该开始了解在你的应用程序的什么地方应该考虑引入松散耦合,接口编程,并减少类之间的依赖。我们在前面章节第一个需求是可维护性,这通常给出一个好的指示,何时何处考虑减少应用程序中的耦合。通常,越大越复杂的应用程序,它就变的越难维护,因此这些技术更有可能变的有用。当不顾应用程序的类型时这是事实:它可能是一个桌面应用程序,一个网页应用程序,或者一个应用程序。

乍一看,这好像是反直觉的。上面显示的第二个示例代码引入了一个在第一个没有引入的接口,它也需要一些我们没有显示的信息,这些信息代表客户端类负责实例化和管理对象。通过一个小示例,这些技术显现了添加解决方案的复杂度,但是随着应用程序变的越来越大和越来越复杂,这些开销会变的越来越不明显。

之前的示例也阐明了另一个关于在哪里适合使用这些技术的观点。最可能的,ManagementController类存在于应用程序的用户接口层,TenantStore类是数据访问层的一部分。它是一个设计应用程序的公共方法因此在未来它可能替换一个层而不打扰其它层。例如,替换或者添加一个新的UI给应用程序(比如为手机平台创建一个app除了传统的网页UI外)而不改变数据层或者替换底层存储机制不盖被UI层。生成应用程序使用多个层帮助应用程序的各个部分解耦。你应该尝试认同应用程序的部件将来有可能改变,为了最小化、局部化这些改变带来的影响,从应用程序剩余部分分离。

前面章节列出的需求清单也包含横切关注点你可能需要在你的应用程序中以统一的方式给一系列类应用横切。示例包含的关注点通过应用程序块在企业库中处理(https://msdn.microsoft.com/library/cc467894.aspx)比如登录,异常处理,验证,或者瞬时故障处理。 这里你需要确认你需要这这些的哪里处理横切关注点,以便为类添加了功能还不在类里存在添加的信息。这将帮助在应用程序中一致管理功能并引入一个干净的分离关注点。

面向对象设计原则

终于,在进一步讨论依赖注入和Unity之前,关于面向对象编程和设计目前为止我们要涉及5个SOLID原则的讨论。SOLID是以下原则首字母缩略词:

单一责任原则

开放/关闭原则

Liskov替换原则

接口分离原则

依赖倒置原则

接下来的章节将描述这些规则和它们与松散耦合的关系和这章开始时列出的需求。

单一责任原则

单一责任原则声明一个类应该有一个,并且只有一个要改变的原因。获取更多的信息,查看Robert C.Martin的面向对象设计原则的文章。

在这章开始的第一个简单示例中,,ManagementController类有两个责任:在UI中扮演一个控制器并且实例化和管理TenantStore对象的生命周期。在第二个示例中,负责实例化和管理TenantStore对象在系统的另一个类或者组件中。

开放/关闭原则

开放/关闭原则声明“软件实体(类,模块,功能,等等)对于扩展应该开放,但是对修改关闭”(Meyer,Bertrand(1988)。面向对象软件构造。)即便你可能是为了修复缺陷更改代码,你应该扩展一个类假如你要添加给它添加任何新的行为。这将帮助保持代码可维护性和可测试因为存在的行为没有改变,并且新的行为位于新的类中。给你的应用程序添加横切关注点的支持需求能最好的符合开放/关闭原则。例如,当你在你应用程序中添加登录到一个类集合中,你不应该对已经存在的类的实现做任何更改。

Liskov替换原则

Liskov替换原则在面向对象中声明在一个电脑程序中,如果S是T的子类型,那么该程序中,T类型的对象可以被S类型的对象替换而不惊动任何满足需要的属性,比如正确性。

这章的第二段代码示例中显示,ManagementController应该可以像期待的那样继续工作当你传给它一个ITenantStore接口的实现。这个示例使用一个接口类型做为类型传递给ManagementController类的构造器,但是你同样使用一个抽象类型。

接口分离原则

接口分离原则是一个软件开发原则意在使软件更好维护。接口分离原则鼓励松散耦合,因而使系统更容易重构、更改、重新部署。原则声明很大的接口应该拆分成更小和更明确的,那么客户端只需要知道它用得到的方法:没有客户端类应该被强制依赖它用不到的方法。

在这章之前定义的ITenantStore中,假如你决定不是所有的客户端类都会使用UploadLogo方法你应该考虑分离这个方法到一个分开接口正如一下代码示例显示的那样:

public interface ITenantStore
{
void Initialize();
Tenant GetTenant(string tenant);
IEnumerable<string> GetTenantNames();
void SaveTenant(Tenant tenant);
} public interface ITenantStoreLogo
{
void UploadLogo(string tenant,byte[] logo);
} public class TenantStore:ITenantStore,ITenantStoreLog
{
...
public TenantStore()
{
...
}
...
}

依赖倒置原则

依赖倒置原则声明:

高层模块不应该依赖底层模块。两个都应该依赖抽象。

抽象不应该依赖于细节之上。细节依赖于抽象之上。

这章的两个代码示例都阐明了怎么应用这个原则。在第一个示例中,高层ManagementController类依赖于低层TenantStore类。这一般限制了高层类用于另一个上下文的选择。

在第二个示例中,ManagementController类现在依赖于ITenantStore抽象,正如TenentStore类一样。

总结

在这章中,你已经看到了你如何处理一些公共需求在企业应用中比如可维护性和测试性通过为你的应用程序采取一个松散耦合设计。