Chinaunix首页 | 论坛 | 博客
  • 博客访问: 29958746
  • 博文数量: 708
  • 博客积分: 12163
  • 博客等级: 上将
  • 技术积分: 8240
  • 用 户 组: 普通用户
  • 注册时间: 2007-12-04 20:59
文章分类

全部博文(708)

分类: Java

2008-10-14 15:45:40

依赖注入技术(Dependency Injection)被世人所认知和使用有一段时间了。近来,越来越多的人开始使用它的原理来设计、开发、和单元测试程序。一些很优秀很有意思的框架也由此开发出来,比如SpringFramework和Google Guice等等。译者翻译的这篇近两年前的文章,对于人们了解和学习Dependency Injection的基本原理应该是有裨益的。

就Injection目前在业界的直译(依赖注入)而言,译者认为不明了。所以以下就还一直使用英文。另请注意,这项技术并不是将依赖性进行注入,而是一些功能和资源的注入,请读者不要望文生义。

原文:

范畴
这篇文章从较高的层次来一览Injection(DI)技术。它的目的是在如何使用几种DI容器的背景下,把DI技术概念性地介绍给初学者。

是DI还是IOC(反向控制)?
当前的许多资料通常将DI和IOC混为一谈。就我个人而言,DI应该是一个更具体的名字。我能想像在许多情况下使用IOC的时候,不见得最后的模式就是以解决依赖性为重心(比如从一个控制台应用程序到一个窗口事件循环)。但是,如果你更习惯IOC,你基本上可以将以下文章里的DI换成IOC来读。IOC有多种类别,像一类,二类,三类。这些类别间的不同不
在本文的讨论范围之内。
另请注意我在本文里大量使用了服务端(Service)词。请不要将它混淆为以服务为导向的框架(SOA)。我这里的意思只是用用户端来代表用户端组件,服务端来代表服务端组件。

DI的简要介绍

简要介绍

情境一
假设你在一个将出差当成家常便饭的公司工作。通常来说,你乘飞机来旅行。每次你赶飞机时,总是要安排taxi。你认识航空公司订票的人员,出租车公司安排taxi送你到机场的人员。你知道他们的电话,你知道通常订票的谈话内容。你的典型的旅行步骤如:
  • 决定目的地,和希望到达时间;
  • 给航空公司打电话,将必要的旅行信息传达出去,用以订票
  • 给出租公司打电话,要求taxi从你住地送你到机场去赶某一具体航班(出租公司可能需要联络航空公司来得到航班的起飞时间表,机场信息,并计算从你住地至机场的的具体时间,及相应的到达你住地的时间)
  • 获取机票,赶上taxi,开始出差之旅
如果现在你的公司突然更换原先订票的经纪以及相应的交流手段,你可能被迫进入重新熟悉的境地:
  • 新的经纪公司,他们的交流方式(比如说,如果新的经纪通过互联网来做生意,而不是原来的电话)
  • 用以成交的典型的谈话方式的次序(是数据,而不再是声音)
不仅仅是你,很可能你的许多同事也要对此变化进行适应。适应的过程往往要花上可观的时间。

情境二
现在让我们来假设整个程序有一点点不同。你们公司有一个行政部门。当你需要出差旅行的时候,行政部门的互动电话系统会给你打电话(事实上是将你和订票经纪公司挂起钩来)。在电话上,你只需回答特定的一套问题,来讲出你的目的地和需要的到达时间。机票订票系统是专为你们设计的,出租公司将计划好taxi的时间,同时,机票也会给你送上门来。

如果现在订票的经纪更换了的话,行政部门会知会这个变更,也许他们会相应调整订票经纪的工作流程。互动的电话系统可以重新程序化,以便于和经纪在互联网上沟通。但是,你和你的同事们不需要有任何的重新适应过程。你仍旧只需照先前的程序走就行了(所有的调整都由行政部门去做了)

Injection)

在上述两个情境中,你是客户,你依赖于经纪提供的服务。但是,情境二有些不同:
  • 你不需要知道订票经纪人和电话 - 必要的话,公司行政部门会打电话给你
  • 你不需要知道行政部门和订票经纪的交流程序和方式,虽然你知道如何与行政部门一个特定的交流方式
  • 你不需要作出任何调整,即使当给你提供服务的经纪有变更的时候
这就是现实中的依赖性注入(DI)。这种方式看上去省不了你什么,但是当你把它应用到一个大公司里去时,他的节省是很可观的。

在软件领域里的DI

软件的组件(用户端)通常是一整套相互作用的组件中的一部分,这一整套组件又会依赖于一些其他的组件(服务端)来完成它们的预定目标。在许多情况下,它们需要知道和哪些组件打交道,在哪里可以找到那些组件,以及如何和那些组件交流。如果获取到这些组件(服务端)的方式改变时,这些变化可能迫使大量用户端的组件作出调整。

有鉴于此,把代码结构化的一个方法是,将找到和实例化服务端组件的逻辑嵌入到用户端的组件里。另一个方法是将客户端对服务端的依赖性声明,让一些‘外在’的代码来承担找到和实例化服务端组件的任务,并提供相关的服务端的引用(reference)给客户端。后一种方式里,当寻找外在依赖性的方式变更时,客户端的代码通常不需要调整。这种实施的方法被认为是DI,前面提到过的那些’外在‘的代码可能是自己亲自写的,也可能是按一些现成的DI框架来实施的。

那些试着将上述情况在设计术语中得到印证的人会发现他们之间的可比性适用于三个不同的术语 - DI,工厂(Factory),和程序接口(不是具体的实施)。在这个比较中,这三个术语通常被用到(不一定同时用到)。

一直到几年前,传统的方法总是将接口和实施分离开来。工厂设计模式甚至允许将复杂的实例化隐藏起来。但是,找到服务端组件的机制就交给客户端去做了。而且,部分的服务端组件需要晓得其内部各组件之间的依赖关系,在实例化的过程中暗含的次序问题,以及跟踪和他们的生命周期。

举例来说,J2EE中使用了JNDI来标准化找寻对象引用的机制。这样的实施在一些简单的,没有JNDI的环境中就会迫使客户端的代码作出调整。

DI的使用要求我们在写软件时,把依赖性搞清楚,让DI框架里的容器替我们去做那些将服务端部件实例化、初始化、次序化、然后提供用户端需要的服务端的引用的复杂工作。

DI框架举例

现在有不少对开发人员有帮助的框架。我发现确实有用的有:
  • 框架:一个相当大的框架,提供包括DI在内的很多功能
  • :一个很小,但是集中于DI容器的框架
  • :另一个DI容器的框架
  • :一个非常有效地利用DI的,主要基于命令模式的框架。虽然它是独立存在的,但它通常和Webwork结合起来用
在下面的章节里,我们将取一种情境,先不用DI来实施一次,再用上述提到的每一种DI框架来实施。这样,我们就能体会到如何使用这些框架。这里,我尽量少用代码,而且这些代码的主要重心也在DI相关的功能上。

实施情境

随附的源代码基本上实施了我之前提到的那个旅行订票情境。总的流程图(从每一个"*Harness"类里的主方法开始)如下:
  • Harness类被实例化,其中的runHarness()方法被调用。
  • runHarness()方法(在AbstractHarness类中)首先激活configure()方法。configure()方法再来对DI容器进行初始化,并注册它的实施类。
  • runHarness()方法随后激活getPlanner()方法。这通常会激活DI框架底层的Lookup方法。目前暂不明瞭的是,这个Lookup方法是否会引发DI容器对AirlineAgency,CabAgency,和TripPlanner等接口的实施类的实例化,并解决好它们之间的相互依赖关系。
  • runHarness()方法随后在调用planTrip()方法来实际完成订票和计划taxi接人的时间问题。
值得注意的依赖关系如下:
  • AirlineAgency和CabAgency是代表相应服务的接口程序。CabAgency需要对AirlineAgency有一个引用才能完成它的功能。
  • TripPlanner是一个在与AirlineAgency和CabAgency结合起来后来完成旅行计划的接口程序。TripPlanner需要对AirlineAgency和CabAgency的引用。
基于源代码的UML图如下所示:



在各个不同框架下应注意到的显著的差别是:
  • harness类里的configure()和getPlanner()方法
  • 接口和实施类的关系是如何描述的(有的使用Java,有的使用文件)
  • 不同服务组件间的依赖关系是如何声明的(有的使用XML文件,有的使用constructor,有的使用setter的方法)
  • 其他的需求期望(比如说,xwork里的接口推动者-enabler interfaces)
无容器实施

对于无容器实施而言,无一定之规。一些实施会做服务组件的查询(比如JNDI)或建立一组工厂的接口程序(可能通过单态模式来实施)。在我们这个case里,我选择的是首先从属性文件里读取类名,然后使用映射的方式来将各服务组件里的对象实例化。

属性文件如下(只有类名):
airline-agency-class = com.dnene.ditutorial.common.impl.SimpleAirlineAgency
cab-agency-class = com.dnene.ditutorial.common.impl.ConstructorBasedCabAgency

用来实例化AirlineAgency,CabAgency,和TripPlanner的源代码如下:
AirlineAgency airlineAgency = null;
CabAgency cabAgency = null;
TripPlanner tripPlanner = null;
// Get class names from property file
InputStream propertyFile = getClass().getClassLoader().
getResourceAsStream("nocontainer-agency.properties");
Properties properties = new Properties();
properties.load(propertyFile);

Class airlineAgencyClass = Class.forName
(properties.getProperty("airline-agency-class"));
Class cabAgencyClass = Class.forName
(properties.getProperty("cab-agency-class"));
Class tripPlannerClass = Class.forName
(properties.getProperty("trip-planner-class"));

if (AirlineAgency.class.isAssignableFrom(airlineAgencyClass))
{
// Instantiate airline agency
airlineAgency = (AirlineAgency) airlineAgencyClass.newInstance();
}
if (CabAgency.class.isAssignableFrom(cabAgencyClass))
{
// Instantiate CabAgency, and satisfy its dependency on an airlineagency.

Constructor constructor = cabAgencyClass.getConstructor
(new Class[]{AirlineAgency.class});
cabAgency = (CabAgency) constructor.newInstance
(new Object[]{airlineAgency});
}
if (TripPlanner.class.isAssignableFrom(tripPlannerClass))
{
Constructor constructor = tripPlannerClass.getConstructor
(new Class[]{AirlineAgency.class,CabAgency.class});
tripPlanner = (TripPlanner) constructor.newInstance
(new Object[]{airlineAgency,cabAgency});
}
return tripPlanner;

请注意对AirlineAgency的引用被传递给了CabAgency的constructor(这表示两服务组件间的依赖关系)。
实际旅行规划操作的代码是在AbstractHarness和AbstractTripPlanner中。它在所有的harness类重复使用。

AbstractHarness.java的部分代码如下:
public void runHarness() throws Exception
{
this.configure();
this.getPlanner().planTrip(
"1, Acme Street",
"11111",
"22222",
new Date(System.currentTimeMillis() + 7200000));
}
AbstractTripPlanner.java的部分代码如下:

public void planTrip(String departingAddress, String departingZipCode,
String arrivalZipCode, Date preferredArrivalTime)
{
FlightSchedule schedule = airlineAgency.bookTicket
(departingZipCode,arrivalZipCode,preferredArrivalTime);
cabAgency.requestCab(departingAddress ,schedule.getFlightNumber());
}

Pico容器的实施
容器的实例化

这里要求一个简单的对DefaultPicoContainer类的实例化。
pico = new DefaultPicoContainer();
组件的注册
在Pico的容器里注册实施类。
pico.registerComponentImplementation(SimpleAirlineAgency.class);
pico.registerComponentImplementation(ConstructorBasedCabAgency.class);
pico.registerComponentImplementation(ConstructorBasedTripPlanner.class);

依赖性的声明
依赖性是将包含着某一特定服务组件所依赖的接口程序列表的一个constructor里声明的。在下面的情况里,
ConstructorBasedCabAgency的服务件是依赖于AirlineAgency的接口程序的。
public ConstructorBasedCabAgency(AirlineAgency airlineAgency)
{
this.airlineAgency = airlineAgency;
}
类似地,ConstructorBasedTripPlanner也在它的constructor里声明对AirlineAgency和CabAgency的依赖。

依赖性的解决
Pico通过查看constructors来解决依赖性的问题。服务组件借助于对具体实施类的注册而完成在Pico容器中的注册。用户组件
通过那些接口类的名字来调用服务组件。智能化的Pico容器将遍历接口实施的层次和等级来返回适合用户需求的实施。(请注意在前面讨论过的
无容器实施方案也是通过类名来实例化实施类的)。

因为TripPlanner接口是依赖于Airline和CabAgency两个接口的,CabAgency接口又是依赖于AirlineAgency接口的,Pico容器
会自动将AirlineAgency首先实例化,然后在建CabAgency时,将对AirlineAgency的引用传递过去;在建TripPlanner时,
将对AirlineAgency和CabAgency的引用传递过去。在下面的需求提出时,上面整个的次序得以执行:

pico.getComponentInstanceOfType(TripPlanner.class);

HiveMind的实施

XML的声明
在这个例子里,接口和实施类都在xml文件里来声明。请注意各服务端组件间的依赖性不在xml文件里声明,而是在随后的代码里。
xml文件里的一个服务点将记录下一个服务组件所需的必要数据。

















容器的初始化

这一步会自动按照上面xml文件里注明的值将注册调配妥当。
Registry registry = RegistryBuilder.constructDefaultRegistry();

依赖性声明
CabAgency对AirlineAgency的依赖性通过在相关setter方法中的定义暗中传达给了HiveMind容器。
public void setAirlineAgency(AirlineAgency airlineAgency){
this.airlineAgency = airlineAgency;
}

依赖性的解决方案
当调用TripPlanner的引用时,依赖性的解决方案同样是自动进行的(见以下部分代码)。HiveMind能够
遍历所有依赖性的结构,从而来依次对AirlineAgency,CabAgency,和TripPlanner接口来进行实例化,并
在CabAgency中激活setter(setAirlineAgency),在TripPlanner中激活两个setter(setAirlineAgency,
setCapAgency),从而来解决它们之间的依赖性。

registry.getService(TripPlanner.class);

Spring框架的实施

XML声明

需要实例化的实施都将在XML里声明。各服务组件间的依赖性作为属性元素来声明。Spring框架将使用这些信息来激活相应的
setter方法。


















容器的初始化

容器的初始化通常需要对xml文件的引用和对bean工厂的实例化。
ClassPathResource res = new ClassPathResource("spring-beans.xml");
BeanFactory factory = new XmlBeanFactory(res);
依赖性的解决方案

调出对服务端组件的引用是基于定义在xml文件里的‘id’,而不是接口类。同样地,所有的服务组件按照准确的次序隐性地实例化,setters得以调用来解决依赖性问题。

factory.getBean("TripPlanner");

XWork实施


XWork其实是有一定DI功能的命令行模式的框架。它可能是在所有DI框架中最不成熟的了。但是,我发现如果你已经决定使用
webwork,特别是你要把依赖性注入到操作类中去的时候,XWork是一个很有用的框架。

XML声明


application
com.dnene.ditutorial.common.impl.SimpleAirlineAgency
com.dnene.ditutorial.xwork.AirlineAgencyAware


application
com.dnene.ditutorial.common.impl.SetterBasedCabAgency
com.dnene.ditutorial.xwork.CabAgencyAware


application
com.dnene.ditutorial.common.impl.SetterBasedTripPlanner
com.dnene.ditutorial.xwork.TripPlannerAware

请注意XWork依赖于一个叫做‘推动者‘的接口(enabler interface)。它基本上是一个为服务组件定义setter方法的接口。
public interface CabAgencyAware
{
public void setCabAgency(CabAgency cabAgency);
}
和其他的容器不同的是,XWork要求我们定义AirlineAgencyAware,CabAgencyAware和TripPlannerAware 接口,并
正确地实施它们。从这个角度来看,XWork要比其他框架烦人些。

容器的初始化

这看上去会有一点复杂。
ComponentConfiguration config = new ComponentConfiguration();
InputStream in =
XworkHarness.class.getClassLoader().getResourceAsStream("components.xml");
config.loadFromXml(in);
cm = new DefaultComponentManager();
config.configure(cm,"application");
依赖性的声明

各类通过对必要的’推动者接口‘的实施来声明它们之间的依赖性。因此每一个服务组件通过实施相应的’推动者接口‘来声明
它对其他组件的依赖性。XWork使用’推动者接口‘来解决依赖性。
public class SetterBasedCabAgency extends AbstractCabAgencyimplements AirlineAgencyAware{ ... }
依赖性的解决方案

很自然的,从一个组件管理器引发的一个查询就可以自动解决所有的依赖性问题。

cm.getComponent(TripPlannerAware.class);
XWork和WebWork
XWork通常和WebWork被绑在一起使用。在那样的情况下,你需要做的就是定义并实施推动者接口类,写出XML文件,
并使用组件拦截器。组件管理器间的互动得以通过WebWork来实施,所以只要将操作类的依赖性加以解决,组件管理器的
代码是不需要的。此外,对不同的组件管理器设立不同的范畴(比如说,应用,session,或request)是很容易的。

结论


正如你所看到的,每一个DI框架在实施它的功能上都或有不同。平心而论,它们在使用上也有不同(比如说,Pico容器
是基于Setter的DI,而Spring框架则是基于constructor的DI)。我刚刚决定要记录下其中的一种已被支持的机制。
从全面的角度来考量,要说一个框架比另一个框架好有点难。我想你需要根据你自身的实际情况来使用最合适的。从以上情境来看,最清晰不过的是,使用DI框架将显著地降低实例化不同组件以及解决它们之间的依赖性的复杂程度。

额外功能

请注意我主要把重心放在下面两方面:
  • 解决用户端对服务端组件的依赖性
  • 解决服务端各组件间的相互依赖性
DI框架其实提供了更多的功能-主要在服务组件的配置和生命周期的管理上。就这些功能,你应该读相关的文档以了解得更多些。就大多数具有中小型复杂度的应用而言,DI本身就已经足够好了。但是,当你的应用变得更复杂的时候,你可能就需要探询些其他的功能。本文之后的一些资源列表可以作为获取更多信息的出发点。


一些有争议的话题


在一些领域里,有着相当程度的争论(通常来自于不同DI框架的作者或支持者)。其中的一些如:
基于constructor或基于setter的DI?
这里是指用constructor和用setter来声明依赖性,哪一个更好些?我不确信是否能干净利落地证明一个比另一个好。但是我
注意到,当整体的依赖性结构变得更复杂时,基于setter的DI看上去更易于管理。
XML或Java?
一些人偏好将依赖性的关系用Java代码来编织起来,更多的人则偏向于XML。我认为用Java来做更简单些。但是至少在
一个境况下,XML是唯一可行的选择。那就是当你推出二进制的产品时,为了系统管理员、集成人员、最终用户能够通过插入
为服务组件而做的不同的实施(为布署系统做的一些特别改动)而修改和添加默认的功能。你的最终用户更可能来改动你的XML
文件(假设你的文档做得好的话),而不是你的源代码(假设你已经公布了)。
在上述情况下,更重要的是需要指出,争议是关于方法,而不是结果。不管你用何种方法来实施,你都能利用DI框架。注意在
许多情况下,你会有两个选择(你可能需要好好研究一下API的文档)。如果框架的文档推荐使用XML,你很可能可以使用Java
来串起来,只要你能搞清楚那些能这么做的API就行。如果文档建议你用的框架只支持基于constructor或setter的DI,那么
一定要深度探索一下,很有可能你会发现对其他方法的支持。
使用DI能得到什么?
对DI从整体来看,无论是作为一个设计模式,还是不同的框架,你可能都会问,我能从DI上得到些什么?以下就是一些好处
  • 如果审视你的服务组件依赖性,它会包含两个子依赖性。一个是基于API或服务组件自身协议的专于某一领域的依赖性。另一个是泛指基于服务组件间实例、配置、和查询的技术协议上的依赖性。DI允许你将处理后者的代码转移到附带的代码边界上去(如XML文件或一些离你的’主‘方法不远的方法里)。这样,一些技术上的更改得以较容易地处理,因为客户端组件的开发人员可以集中精力于他们最拿手的-服务端某一特定领域的API。
  • 如果你想创建即插即用的结构,从而你的最终用户和系统集成人员可能会要求延伸或更改服务端的功能,那么你用DI的方法来做会让事情变得很容易。DI让按来建造软件变得容易。在举例中,我提到作为一个DI容器的Spring,其实Spring本身也大量地使用了DI。建造进Spring的一系列功能(如AOP,交易处理等)实际上是用DI来插入这些即插即用的服务的。Spring框架也可以靠写一些额外的服务并用DI来插入它们得以延伸和拓展(比如说,spring模块,参见http://springmodules.dev.java.net)。
  • 如果你真的在应用中,建立大量的细分的彼此间相互依赖的服务,很可能你要重新布局的次数,比你预计的要来的多。如果你真的用了DI的框架的话,你会很庆幸你当初的选择。
  • 当你最初的假设开始变更的时候,你需要开始加强你的应用。比如,你有建在J2EE平台上的服务组件,它们之间靠JNDI来相互查询。如果你现在把代码移植到一个没有JNDI的环境中去,如果你当初用了DI的话,就没有一点问题。
阅读(1486) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~