首页 > 代码库 > 依赖注入与Unity(一) 介绍
依赖注入与Unity(一) 介绍
在你学习依赖注入和Unity之前,你需要明白你为什么要使用它们。为了明白为什么要使用它们,你应该明白依赖注入和Unity能够帮助你解决什么类型的问题。作为介绍部分,这一章不会涉及太多关于Unity和依赖注入的东西,但是会提供必要的背景信息,以便让你了解依赖注入这种技术的好处,以及Unity的原理。
第二章“依赖注入”将会展示依赖注入式怎样解决本章提及的需求的,第三章“Unity与依赖注入”展示Unity在你的程序中如何实现依赖注入。
动机
当你设计和开发软件系统的时候,有许多需求需要考虑。一些需求比较特殊,而另一些需求则在目的上有通用性。可以将这些需求分为功能性需求和非功能性需求(质量属性)。每一个不同的系统都有一套不同的需求。上面提到的非功能性需求是一套通用需求,尤其是具有相对较长的预期生命周期的行业软件系统。非功能性需求并不是在所有开发的系统中都是重要的,但是可以确定的是,在你开发的许多系统的需求列表中包含这些非功能需求。
可维护性
因为系统变得越来越大,并且系统预期的生命周期越来越长,维护这样的系统变得越来越有挑战性。通常该系统原来的开发人员已经不在,或者已经忘记了该系统的详请,文档已过期或者丢失。同时要求业务上反应迅捷,以便满足一些新的,迫切的需求。可维护性是软件系统的一个质量指标,它决定了系统能够容易有效的更新的程度。如果发现了一个必须要修改的缺陷,你就会需要更新系统(即执行矫正维护);如果操作环境发生了变化,你需要对系统进行修改,或者需要为系统添加新的功能以满足一个业务需求(即改善性维护)。可维护的系统增强了架构的灵活性,降低了成本。因此,你应当把可维护性以及其他如稳定性,安全性,可扩展性包含到你的设计目标中。
可测试性
一个可测试的系统是这样一个系统,它可以让你对系统的各个部分进行单独的测试。设计和编写有效的测试与设计和编写可测试的程序代码一样具有挑战性,尤其是当一个变得系统越来越大,越来越复杂的时候。像测试驱动开发(TDD)这样的方法学需要你在写任何代码来实现一个新的功能之前编写单元测试,并且,这样一个设计技术(TDD)的目标是提升你的程序的质量。这样的设计技术也可以帮助你扩展你的单元测试的覆盖面,减少回归的可能性,使重构更加容易。然而作为测试过程的一部分,也应该包含其他类型的测试像验收测试,集成测试,性能测试以及压力测试。
运行测试也是要花费时间和金钱的,因为需求需要在现实环境中测试。例如,一些类型的测试是在基于云的应用程序上进行的 ,你需要将应用程序部署到云环境中,在云中运行测试。如果你使用TDD, 因为部署应用程序到云上,甚至是本地的模拟器上所花费的时间使得在任何时候将所有的测试运行在云上是不切实际的。在这种类型的场景中,你可能决定使用复制品(简单的stubs或可证实的模拟) 来取代云环境中实现测试的真实组件 ,以便能够让你在标准TDD开发周期中在隔离的环境中运行你的单元测试套件。
可测试性应该是你系统的另一个设计目标,就像可维护性和敏捷性一样:一个可测试的系统更加是可维护的,反之亦然。
灵活性和可扩展性
灵活性和可扩展性是企业应用程序经常需要的质量属性。因为不管是在程序的开发阶段,还是在作为产品运行之后,业务需求会经常变化,所以应当将程序设计的灵活的,使得程序可以适用不同的工作方式,以及可扩展的,使得程序可以添加新的功能。例如,你可能需要将程序从用户或组织所在的经营场所计算机中转换到运行在云上。
晚绑定
在一些应用程序中,可能会有支持晚绑定的需求。如果想要在不重新编译的前提下替换程序的一部分功能,那么晚绑定是很有用的。例如,你的程序可能会支持多个关系数据库,每个数据库对应一个独立的模块。你可以通过配置文件的方式告诉程序在运行的时候使用指定的数据库模块。另一个晚绑定非常有用的场景是允许程序系统的用户以插件的方式实现程序系统的自定义。
并行开发
当你正在开发大型(或者甚至是小型或中型)系统的时候,让整个开发团队同时开发一个功能或组件是不切实际的。实际上,你会将不同的功能或组件分配给不同的小组进行并行开发。尽管这种方式可以让你缩短项目的持续时间,但同时也引入的额外的复杂性:你需要管理多个小组并正确的将不同小组开发的模块整合到一起。
交叉关注点
企业应用程序通常需要致力于一系列交叉关注点如验证、异常处理和日志。你可能需要在程序的许多地方使用这些功能并且将会用标准的持续的方式来实现它们以提高系统的可维护性。理想情况下,你希望找到一种机制在不需要修改现有类的前提下有效、清楚地为对象在设计时或运行时添加行为。通常,你需要在程序运行时灵活的配置这些功能,并且在一些情况下,对现有的系统添加功能来解决交叉关注点问题。
松耦合
如果你能够确保你在设计中,程序是由许多部分松耦合而成的,那么,就可以解决前面部分提到的许多需求。松耦合,与紧耦合相对,意味着它降低了组成系统的组件之间的依赖数。这使得修改系统的一部分变更更加容易和安全,因为程序的每一部分在很大程序上与程序的其它部分是相互独立的。
一个简单的例子
下面是一个紧耦合的例子。ManagmentController类直接依赖于TenantStore类。这些类可以分布在不同的Visusal Studio 项目中。
1 public class TenantStore 2 { 3 ... 4 public Tenant GetTenant(string tenant) 5 { 6 ... 7 } 8 public IEnumerable<string> GetTenantNames() 9 {10 ...11 }12 }13 14 public class ManagermentController15 {16 private readonly TenantStore tenantStore;17 public ManagermentController()18 {19 tenantStore = new TenantStore(...);20 }21 public ActionResult Index()22 {23 var model = new TenantPageViewData<IEnumerable<string>>24 (this.tenantStore.GetTenantNames())25 {26 Title = "Subscribers";27 };28 return this.View(model);29 }30 31 public ActionResult Detail(string tenant)32 {33 var contentModel = this.tenantStore.GetTenant(tenant);34 var model = TenantPageViewData<Tenant>(contentModel)35 {36 Title = string.Format("{0} details",contentModel.Name)37 };38 return this.View(model);39 }40 41 ...42 }
在这个例子中,TenantStore类实现了一个存储库,该库处理对数据存储如关系数据库的访问。ManagmentController类是一个MVC控制器类,从存储库请求数据。注意,ManagmentController在调用GetTenant和GetTenantNames之前必须实例化一个TenantStore对象,或获取一个TenantStore对象的引用。ManagmentController依赖于特定的,具体的TenantStore类。
如果你回顾一下本章前面提到的企业应用程序一些值得引入的通用需求,你就会评估出上面的代码很好的契合这些需求。
尽管这个简单人例子只展示了一个客户端类(本例中客户端类为ManagemntController类)使用TenantStore类的情况,实际上,可能在程序中许多客户端类都会用到TenantStore类。假定每一个客户端类都在运行时(runtime)负责初始化或定位(locating) TenantStore对象,那么所有的这些客户端类都束缚于TenantStore类的一个构造函数或初始化方法,并且当TenantStore类的实现发生改变的时候,所有的客户端的类可能也要做出相应的变化。这就潜在的使得TenantStore类的维护更加复杂,更加容易出错,更浪费时间。
为了在ManagmentController类的Index和Detail方法上运行单元测试,必须初始化TenantStore对象并且保证下层的数据存储包含合适的测试数据。这使得测试过程复杂化,并且依赖于正在使用的数据存储,可能会使运行测试时更加消耗时间,因为必须创建和操作数据存储中的正确数据。这也使得测试更加脆弱。
可以改变TenantStore类的实现以使用不同的数据存储,例如使用Windows Azure table storage来代替SQL Server。然而,使用TenantStore类的那些客户端类可能也需要做一些修改,如果这些客户端类需要提供一些初始化数据如连接字符串。
这种方式下不能使用晚绑定,因为客户端类直接使用了TenantStore类而被编译。
如果想要添加对交叉关注点的支持如为多个存储类(包括TenantStore类)添加日志功能,需要单独的修改和配置每一个存储类。
下面的例子展示了一个小的变化,客户端类ManagmentController的构造函数接收一个实现了ITenantStore接口的对象,同时TenantStore类实现了该接口。
1 public interface ITenantStore 2 { 3 void Initialize(); 4 Tenant GetTenant(string tenant); 5 IEnumerable<string> GetTenantNames(); 6 void SaveTenant(Tenant tenant); 7 void UploadLogo(string tenant, byte[] logo); 8 } 9 10 public class TenantStore:ITenantStore11 {12 ...13 public TenantStore()14 {15 ...16 }17 ...18 }19 20 public class ManagmentController:Controller21 {22 private readonly ITenantStore tenantSotre;23 public ManagmentController(ITenantStore tenantSotre)24 {25 this.tenantSotre = tenantSotre;26 }27 28 public ActionResult Index()29 {30 ...31 }32 33 public ActionResult Detail(string tenant)34 {35 ...36 }37 ...38 }
这个变化对满足前面列出的非功能性需求有直接的影响。
现在已经清楚了,ManagmentController类和其它任何使用TenantStore类的客户端类都不负责TenantStore对象的实例化,尽管这个例子中并没有为我们展示到底是哪个类或组件负责TenantStore对象的实例化。从维护的角度看,实例化TenantStore对象的责任不再属于多个类,而是只属于一个类。
通过控制器类的构造函数的参数我们也能清楚该类所依赖的内容,而不是隐藏在控制器类方法的内部。
为了测试客户端类如ManagmentController类的一些行为,你可以创建一个ITenantStore接口的简单实现,可以返回一些简单的数据。而不需要创建一个需要从下层数据存储中获取数据的TenantStore对象。
引入ITenantStore接口使得而不修改客户端类来替换TenantStore实现变得更加容易。如果接口和其实现分属不同的项目,那么包含客户端类的项目只需要引用包含接口的项目即可。
负责实例化TenantStore对象的类可以为程序提供额外的服务。这个类可以控制由它所创建的ITenantStore对象的生命周期,例如,每一次ManagmentController类需要一个TenantSotre实例的时候,就创建一个新的TenantSotre对象,或者,只维护一个单独的TenantSotre实例,当客户端类需要它的时候,将该实例的引用传递给客户端类。
现在就可以使用晚绑定了,因为客户端类只引用了ITenantStore接口类型。程序可以在运行的时候创建一个实现了该接口的对象(可能是基于配置文件),并且将它传递给客户端类。例如,程序可能依靠web.config文件中的一个配置创建了一个SQLTenantStore实例或一个BlogTenantStore实例,然后将它传递给ManagmentController类的构造函数。
如果接口定义达成一致,两个项目组可以在开发store类和contoller类上并行工作。
负责创建store类实例的类可以在将store实例传递给到客户端类之前添加对交叉关注点的支持,如通过使用装饰模式传递进来一个实现了交叉关注点的对象。不需要修改客户端类或store类来添加对交叉关注点如日志或异常处理的支持。
上面第二段代码所使用的方式是一种使用接口的松耦合设计。如果去掉两类之间的直接引用,就降低了层级耦合,并且加强了解决方案的可维护性、灵活性,可测试性,可扩展性。
什么时候使用松耦合设计?
开始学习依赖注入和Unity之前,你应该明白在程序的什么地方需要考虑松耦合,使用接口编程,降低类与类之间的依赖。在前面的部分我们首先提到的需求是可维护性,它会指示我们应该什么时候在程序的什么地方降低耦合。通常,越大越复杂的程序,越难维护,因此这些技术是有用的,不管程序的类型是桌面应用程序,web应用程序或云应用程序。
乍一看,似乎是违反直觉的。上面第二个例子引入了接口,第一个没有。第二个例子也需要少量的我们在上面没有展示的内容,即代表客户端类负责实例化和管理对象。对于一个小例子,这些技术似乎是增加了解决方案的复杂性,但是当一个程序变得越来越大,越来越复杂时,这个瓶颈就会变得越来越不重要了。
前面的例子向我们展示了关于什么地方使用这些技术合适的另一个一般点。通常情况下,ManagmentController类存在于程序的用户接口层,而TenantStore类是数据访问层的一部分。这个例子展示的是程序设计的一种通用方式,这样未来当需要替换某一层时就不会干扰到其它层。例如,替换或添加一个新的UI(如在传统的web UI之外创建一个移动平台app)而不用改变数据层或替换下层的数据存储机制,也不用改变UI层。使用分层的方式设计程序,可以对各个部分之间进行解耦。你应该能够清楚未来程序可能需要变化的部分,为了最小化和局部化这些改变,应该将它们与程序的其它部分解耦。
面向对象设计原则
最后,在学习依赖注入和Unity之前,叙述一下面向对象编程和设计的5个原则,即SOLID.所谓SOLID即下面5个原则的首字母缩略词:
1. Single responsibility principle: 单一职责原则
1. Single responsibility principle: 单一职责原则
2. Open/close principle: 开放封闭原则
3. Liskov substitution principle:李氏替换原则
4. Interface segregation principle:接口分离原则
5. Dependency inversion principle:依赖倒置原则
单一职责原则
一个类如果要改变,那么只能有一个原因。也就是说,一个类只实现一个功能。在上面的第一个例子中,ManagmentController类有两个职责:作为UI的控制器、实例化TenantStore和管理TenantStore对象的生命周期。在第二个例子中,实例化和管理TenantStore对象生命周期的职责放在系统中的另外一个类或组件中。
开放封闭原则
软件的实体(类,模块,函数等等)应当对扩展开放,对修改封闭。
尽管你可能会因为要修复一个缺陷而去修改这个类,但是当你需要为这个类添加新的行为时,应当去扩展这个类。这有利于保持代码的可维护性和可测试性,因为已经存在的行为不应当被修改,新的行为应当放在新类中。该原则很好的迎合了在程序中添加对交叉关注点支持的需求。例如,当你为程序中的某些类添加日志功能时,不应该修改这些已经存在的类的实现。
李氏替换原则
如果S是T的子类,类型T的对象可以被类型S的对象替换。即子类可以替换父类。在第二个例子中,如果传递任何一个实现了ITenantSotre接口的对象给ManagmentController类,该类也会像预期的一样工作。这个例子中使用的是接口,同样也可以使用抽象类。
接口分离原则
一个大的接口应该被分成多个小的、具体的接口,这样客户端类仅需要知道它要使用的方法,客户端类不应该被强迫依赖于它不使用的方法。
依赖倒置原则
>高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
>抽象不应当依赖于细节,而细节应当依赖于抽象。在上面的第一个例子中高层的ManagmentController直接依赖于低层的TenantStore.这通常会限制在另一个上下文中重新使用这个高层的类。在第二个例子中ManagmentController依赖于ITenantStore这个抽象,TenantStore同样依赖于抽象。
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。