原文
标题:Dependency Injection for Loose Coupling
作者:Billy McCafferty.
链接:http://www.codeproject.com/cs/design/DependencyInjection.asp
翻译开始时间:2007-11-25
简介
在面向对象的设计中,有一个重要的原则 -- “解耦”。简单说(loosely),不是一语双关(这里使用loosely和loose coupling没有任何关系,只是语法相似),“解耦”的意思就是说,一个对象工作时需要依赖一些对象,而这些依赖应该越少越好。此外,当可能的时候,对象依赖的应该是接口,而不是具体具体化的类。(具体得累就是用关键字“new”常见的实例。)“解耦”能促进更好的重用、增加可维护性,并且允许你能很容易地提供“模仿”(Mock)对象替代昂贵的服务,例如端口交流器(socket-communicator)。
“依赖注入”("Dependency Injection",简称DI),和更神秘的概念“控制反转”(Inversion of Control" ,简称IoC),是一项提供解耦方式的技术。主要有两个实现DI的的方式:构造函数注入(constructor injection)和设置方法注入(setter injection)。显然,在某些点上,必须要“某个东西”有创建具体的类的职责,以提供对象,可供注入到另一个对象。这个注入者(injector)可以为一个父类,我们称为“依赖注入控制端”(DI controller);或者能被一个“依赖注入容器”(DI container)在外部处理实现。以上就是几个主要的使用依赖注入的概观。
构造函数注入(Constructor Injection)
使用构造函数参数传递对象依赖的DI(依赖注入以下都简称为DI)技术,就是构造函数注入。以下的这个例子,包括一个类,Customer,它暴露了一个方法去获得每份销售订单所属客户的详细日期信息。因此,Customer类需要一个数据访问类与数据库关联。假设,现在有一个类orderDao (全名"order data-access object"),实现接口IOrderDao.有一种方法,Customer类可以执行以下代码获得依赖:
IOrderDao orderDao = new OrderDao();
这样做,有2个主要的缺点。
1、在本地实例化OrderDao,就抵消了在一开始使用接口的好处;并且
2、OrderDao不能轻易地被用来测试的Mock对象替换。(Mock对象会稍后讨论)
上述的例子应该如下:
public class Customer {
public Customer(IOrderDao orderDao) {
if (orderDao == null)
throw new ArgumentNullException("orderDao may not be null");
this.orderDao = orderDao;
}
public IList GetOrdersPlacedOn(DateTime date) {
// code that uses the orderDao member
// get orders from the datasource
}
private IOrderDao orderDao;
}
在以上例子中,注意构造函数接受的是一个接口;不是接受一个具体的类。同时,注意如果orderDao参数为null的时候,抛出了异常。这样强调了获取依赖对象的合法性。以我的观点,构造函数注入,它最好的机制在于提供了对象必须的依赖。让开发这清楚地知道,调用Customer,在它能正常执行之前,需要提供哪些它所依赖的对象。然而,考虑以下状况……假设你的类有十个方法都不需要依赖,但你加一个方法需要依赖于IOrderDao。你的确可以改动构造函数,使用构造函数注入,但这会强迫你在所有地方改变原有的构造函数调用。当然,你也可以选择新加一个构造函数老获取依赖,但怎么样能让开发简单地知道使用哪个构造函数呢?最后,如果这个依赖的创建是很昂贵的,为什么要创建它并传入构造函数类,然而却很少使用它?设置方法注入(setter injection)可以在这种情况下使用。
设置方法注入(Setter Injection)
设置方法注入不会强迫传递依赖对象到构造函数中。取而代之,是通过对象暴露出来的公有方法设置依赖。这样做法的动机主要包括以下几点:
1、在继承的类中,无需修改构造函数,就可实现依赖注入,而且;
2、允许尽可能晚的,而且在需要的情况下,才创建某些昂贵的资源或服务。
以下代码使用设置方法注入替代刚才的构造函数注入:
public class Customer {
public Customer() {}
public IOrderDao OrderDao {
set { orderDao = value; }
get {
if (orderDao == null)
throw new MemberAccessException("orderDao" +
" has not been initialized");
return orderDao;
}
}
public IList GetOrdersPlacedOn(DateTime date) {
// code that uses the OrderDao public
// property to get orders from the datasource
}
// Should not be called directly;
// use the public property instead
private IOrderDao orderDao;
}
在以上的例子中,构造函数没有参数。替代的是,在调用对象时,执行GetOrdersPlacedOn方法之前需要先设置IOrderDao依赖。构造函数注入而言,当依赖一开始没有注入时,程式立刻会抛出异常。例如,创建对象之前。对于设置方法注入而言,异常要到某个方法要使用依赖时才会抛出。还有注意的是,GetOrdersPlacedOn方法使用的是OrderDao属性,而不是直接使用私有的orderDao变量。这样,属性的getter方法才有机会验证依赖对象是否初始化。
使用设置方法注入替换构造函数注入时要谨慎,因为:
1、至少到“has not been initialized”抛出异常的时候,开发人员要清楚哪些依赖是需要的;
2、加大了一点难度去跟踪异常的来源和原因。
虽然如此,设置方法注入能提供新方法的同时尽量少改动原有的代码;如果所依赖的对象创建时非常昂贵或很困难的,此注入方法能提供性能上的加速。
注入器(The Injectors)
紧接着,下一个问题就是,什么东西能创建出依赖对象,注入到“被注入者”?有2个试点那个的地方以供创建这些逻辑:控制器和容器。
DI 控制器(DI Controllers)
DI控制器的创建步骤是容易理解和实现的。在一个适当分层的架构中,一个应用程式拥有不同的层处理逻辑。最简单的分层通常包括与数据库通讯的数据层,负责显示UI的表现层,履行业务逻辑的区域逻辑层。即使没有很好地定义,“控制器”层总是存在,为了定位UI事件到区域和数据层,反之亦然。举个例子,在ASP.NET中,code-behind页面扮演基础控制层。还有更多形式化的控制层存在:Java中的Struts和Spring;.NET的Front Controller 和 Spring .NET 。全部这些实现都遵守着MVC(Model-View-Controller )模式形态。不管你用什么作为你的控制器,这个控制器就是实现依赖注入配件的适合所在;这里是实例化、注入依赖对象的地方。接着两个例子演示使用控制器实现DI。第一个例子是一个示例性的例子 “生成注入代码”-- 你要自我完成配置。第二个是“测试代码”示例 -- 用来测试应用程式的,但没有完整,并不一定需要一个真实的数据库。
控制器代码实现依赖注入(如,ASP.NET code-behind页面):
// code performed when the controller is loaded
IOrderDao orderDao = new OrderDao();
// Using Setter Injection on a pre-existing customer
someCustomer.OrderDao = orderDao;
IList ordersPlacedToday =
someCustomer.GetOrdersPlacedOn(DateTime.Now);
单元测试的依赖注入:
IOrderDao orderDao = new MockOrderDao();
// Using Setter Injection on a pre-existing customer
someCustomer.OrderDao = orderDao;
IList ordersPlacedToday =
someCustomer.GetOrdersPlacedOn(DateTime.Now);
使用DI控制器注入以来的一个主要好处就是它非常直接、容易地指出创建在何处发生。缺点就是仍然在某处存在依赖的硬编码;即使硬编码处于经常需要变动的位置。另一个缺点在于,现在DI控制器无法轻易地使用mock对象进行单元测试。(但我承认,某些强大的工具如TypeMock,可以在任何情况下生成注入mock对象。但是,类似TypeMock的工具应该只是用在绝对必要的情况下,因为他们引导你不使用“面对接口编程”的习惯。事实上,我推荐在非常困难得测试才使用。)
在ASP.NET,我更喜欢使用Model-View-Presenter (MVP)模式,ASP.NET code-behind 页面创建依赖,通过构造函数注入到presenter。另外,我使用用户控件作为模式的View部分,因此ASP.NET code-behind在用户控件(View视图)和他们的Presenter之间,纯粹扮演着MVP“依赖初始化”的角色。
另一个实现注入的方式,就是使用应用程式容器……
DI容器(DI Containers)
控制反转(Inversion-of-Control)/依赖注入(Dependency-Injection)容器,无论哪一部分细节的事件触发,都可以监测应用程式、注入依赖。举例,当Customer实例创建时,他可以自动被注入所需要的依赖。首先,这个是非常奇怪的一个概念,但这对于管理大型应用程式多个服务依赖非常有效。不同的容器拥有它们各自的机制,提供管理依赖注入的设置。
Spring .NET允许你使用XML文件定义依赖注入。以下例子,Spring.NET XML使用设置方法注入,为ASPX code-behind page提供数据访问对象的依赖:
<spring>
<context type="Spring.Context.Support.WebApplicationContext, Spring.Web">
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<!-- Data Access Object that will be injected into ASPX page -->
<object id="daoFactory" type="MyApp.Data.DaoFactory, MyApp.Data" />
<object type="ViewDetails.aspx">
<property name="DaoFactory" ref="daoFactory" />
</object>
</objects>
</spring>
ASPX code-behind 简单地公开一个名为DaoFactory的属性,当页面被呼叫时,来获取所需要的依赖。到Spring .NET's website 可以获取更详细信息和示例。对于Java开发者,肯定参观过Spring's website。
有许多其他的容器,有些根本并不需要很多的XML管理(你有时会看到令你畏惧的500行壮观XML文件)。对于Java开发者,看看Container Comparison,有很好的对比观点。对于.net开发者,现在的选择比较少(可能是个好事?)。看看一些开源的建议Open Source Inversion of Control Containers in C# 。
使用注入Mock对象做单元测试(Unit Testing with Injected Mock Objects)
据经验所得,使用DI最大的好处就是更干净的单元测试和非常好的可插拔性。由于接口,依赖带来了可插拔性。我们来看看DI怎么有利于单元测试。这是一个经常的用例,开发者应对真实数据库编写单元测试。避免让单元测试成为累赘,这需要摒弃以前的慢速单元测试 -- 测试业务逻辑层的时候所依赖的数据访问代码,要不断地访问数据库。Mock对象是实现这个的完美选择。Mock对象模仿真实对象的反应,扮演假设使用真正资源的角色。他们可以很好地模仿访问数据库,模仿访问IO,模仿访问Web Service等等。
下面的代码展示了一个Mock的数据访问,实现了一个贯穿全文的IOrderDao接口。
public class MockOrderDao : IOrderDao {
public Order GetOrderById(long orderId) {
Order foundOrder = new Order();
foundOrder.SaleDate = "1/1/06";
foundOrder.ID = orderId;
return foundOrder;
}
}
一个简单的,Mock数据访问对象实现了IOrderDao,因此它能通过构造函数或设置方法注入的方法,传递到各个需要依赖IOrderDao 的对象。现在我们的逻辑层可以脱离访问数据库做测试。在我自己的测试包中,我通常有一段包含真正数据库的测试,然后传入Mock数据库对象以提供业务逻辑对象的单元测试。
引用文章
Inversion of Control Containers and the Dependency Injection Pattern
Unit testing with mock objects