spring等轻量级容器能够帮助开发者将来自不同项目的组件组装成为一个内聚的应用程序.。在它们的背后有着同一个模式,这个模式决定了这些容器进行组件装配的方式。人们用一个大而化之的名字来称呼这个模式:“控制反转”( Inversion ofControl,IoC).给它一个更能描述其特点的名字——“依赖注入”(Dependency Injection),并将其与“服务定位器”(Service Locator)模式作一个比较。不过,这两者之间的差异并不太重要,更重要的是:应该将组件的配置与使用分离开——两个模式的目标都是这个。(应用程序用组件组装起来,但不依赖组件的实现类,将应用程序和组件的实现解耦合)
J2EE 开发者常遇到的一个问题就是如何组装不同的程序元素:如果web 控制器体系结构和数据库接口是由不同的团队所开发的,彼此几乎一无所知,你应该如何让它们配合工作?很多框架尝试过解决这个问题,有几个框架索性朝这个方向发展,提供了更通用的“组装各层组件”的方案。这样的框架通常被称为“轻量级容器”,PicoContainer 和Spring 都在此列中。
所谓“组件”是指这样一个软件单元:它将被作者无法控制的其他应用程序使用,但后者不能对
组件进行修改。也就是说,使用一个组件的应用程序不能修改组件的源代码,但可以通过作者预
留的某种途径对其进行扩展,以改变组件的行为。
服务和组件有某种相似之处:它们都将被外部的应用程序使用。在我看来,两者之间最大的差异
在于:组件是在本地使用的(例如JAR 文件、程序集、DLL、或者源码导入);而服务是要通过
——同步或异步的——远程接口来远程使用的(例如web service、消息系统、RPC,或者
socket)。
在一个真实的系统中,我们可能有数十个服务和组件。在任何时候,我们
总可以对使用组件的情形加以抽象,通过接口与具体的组件交流(如果组件并没有设计一个接口,
也可以通过适配器与之交流)。但是,如果我们希望以不同的方式部署这个系统,就需要用插件
机制来处理服务之间的交互过程,这样我们才可能在不同的部署方案中使用不同的实现。
所以,现在的核心问题就是:如何将这些插件组合成一个应用程序?这正是新生的轻量级容器所
面临的主要问题,而它们解决这个问题的手段无一例外地是控制反转(Inversion of Control)
模式。
几位轻量级容器的作者曾骄傲地对我说:这些容器非常有用,因为它们实现了“控制反转”。这
样的说辞让我深感迷惑:控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这
些轻量级容器与众不同,就好象在说“我的轿车是与众不同的,因为它有四个轮子”。
我想我们需要给这个模式起一个更能说明其特点的名字——“控制反转”这个名字太泛了,
常常让人有些迷惑。与多位IoC 爱好者讨论之后,我们决定将这个模式叫做“依赖注入”
(Dependency Injection)。
依赖注入的形式主要有三种,我分别将它们叫做构造子注入(Constructor Injection)、设值
方法注入(Setter Injection)和接口注入(Interface Injection)
依赖注入的最大好处在于:它消除了应用程序类对具体组件实现类的依赖。这样
一来, 我就可以把应用程序 类交给朋友, 让他们根据自己的环境插入一个合适的
组件实现即可。不过,Dependency Injection 模式并不是打破这层依赖关系的唯一
手段,另一种方法是使用Service Locator 模式。
Service Locator 模式背后的基本思想是:有一个对象(即服务定位器)知道如何获得一个应用
程序所需的所有服务。也就是说,在我们的例子中,服务定位器应该有一个方法,用于获得一个
MovieFinder 实例。在这里,我把ServiceLocator 类实现为一个Singleton 的注册表,于是应用程序就可以
在实例化时通过ServiceLocator 获得一个组件的实例
Dependency Injection 和Service Locator 两个模式并不是互斥的,你可以同时使用它们,
Avalon 框架就是这样的一个例子。Avalon 使用了服务定位器,但“如何获得定位器”的信息
则是通过注入的方式告知组件的。
Service Locator vs. Dependency Injection
首先,我们面临Service Locator 和Dependency Injection 之间的选择。应该注意,尽管我
们前面那个简单的例子不足以表现出来,实际上这两个模式都提供了基本的解耦合能力——无论
使用哪个模式,应用程序代码都不依赖于服务接口的具体实现。两者之间最重要的区别在于:这
个“具体实现”以什么方式提供给应用程序代码。使用Service Locator 模式时,应用程序代
码直接向服务定位器发送一个消息,明确要求服务的实现;使用Dependency Injection 模式
时,应用程序代码不发出显式的请求,服务的实现自然会出现在应用程序代码中,这也就是所谓
“控制反转
控制反转是框架的共同特征,但它也要求你付出一定的代价:它会增加理解的难度,并且给调试
带来一定的困难。所以,整体来说,除非必要,否则我会尽量避免使用它。这并不意味着控制反
转不好,只是我认为在很多时候使用一个更为直观的方案(例如Service Locator 模式)会比
较合适。
Dependency Injection 模式可以帮助你看清组件之间的依赖关系:你只需观察依赖注入的机
制(例如构造子),就可以掌握整个依赖关系。而使用Service Locator 模式时,你就必须在源
代码中到处搜索对服务定位器的调用。
一个关键的区别在于:使用Service Locator 模式时,服务的使用者必须依赖于服务定位器。
定位器可以隐藏使用者对服务具体实现的依赖,但你必须首先看到定位器本身。所以,问题的答
案就很明朗了:选择Service Locator 还是Dependency Injection,取决于“对定位器的依
赖”是否会给你带来麻烦。
人们倾向于使用Dependency Injection 模式的一个常见理由是:它简化了测试工作。这里的
关键是:出于测试的需要,你必须能够轻松地在“真实的服务实现”与“供测试用的‘伪’组件”
之间切换。但是,如果单从这个角度来考虑,Dependency Injection 模式和Service Locator
模式其实并没有太大区别:两者都能够很好地支持“伪”组件的插入。之所以很多人有
“Dependency Injection 模式更利于测试”的印象,我猜是因为他们并没有努力保证服务定
位器的可替换性。这正是持续测试起作用的地方:如果你不能轻松地用一些“伪”组件将一个服
务架起来以便测试,这就意味着你的设计出现了严重的问题。
当然,如果组件环境具有非常强的侵略性(就像EJB 框架那样),测试的问题会更加严重。我的
观点是:应该尽量减少这类框架对应用程序代码的影响,特别是不要做任何可能使“编辑-执行”
的循环变慢的事情。用插件(plugin)机制取代重量级组件会对测试过程有很大帮助,这正是
测试驱动开发(Test Driven Development,TDD)之类实践的关键所在
构造子注入 vs. 设值方法注入
设值函数注入和构造子注入之间的选择相当有趣,因为它折射出面向对象编程的一些更普遍的问
题:应该在哪里填充对象的字段,构造子还是设值方法?
一直以来,我首选的做法是尽量在构造阶段就创建完整、合法的对象——也就是说,在构造子中
填充对象字段。这样做的好处可以追溯到Kent Beck 在Smalltalk Best Practice Patterns
一书中介绍的两个模式:Constructor Method 和Constructor Parameter Method。带有参
数的构造子可以明确地告诉你如何创建一个合法的对象。如果创建合法对象的方式不止一种,你
还可以提供多个构造子,以说明不同的组合方式。
构造子初始化的另一个好处是:你可以隐藏任何不可变的字段——只要不为它提供设值方法就行
了。我认为这很重要:如果某个字段是不应该被改变的,“没有针对该字段的设值方法”就很清
楚地说明了这一点。如果你通过设值方法完成初始化,暴露出来的设值方法很可能成为你心头永
远的痛
不过,世事总有例外。如果参数太多,构造子会显得凌乱不堪,特别是对于不支持关键字参数的
语言更是如此。的确,如果构造子参数列表太长,通常标志着对象太过繁忙,理应将其拆分成几
个对象,但有些时候也确实需要那么多的参数。
如果有不止一种的方式可以构造一个合法的对象,也很难通过构造子描述这一信息,因为构造子
之间只能通过参数的个数和类型加以区分。这就是Factory Method 模式适用的场合了,工厂
方法可以借助多个私有构造子和设值方法的组合来完成自己的任务。经典Factory Method 模
式的问题在于:它们往往以静态方法的形式出现,你无法在接口中声明它们。你可以创建一个工
厂类,但那又变成另一个服务实体了。“工厂服务”是一种不错的技巧,但你仍然需要以某种方
式实例化这个工厂对象,问题仍然没有解决。
如果要传入的参数是像字符串这样的简单类型,构造子注入也会带来一些麻烦。使用设值方法注
入时,你可以在每个设值方法的名字中说明参数的用途;而使用构造子注入时,你只能靠参数的
位置来决定每个参数的作用,而记住参数的正确位置显然要困难得多。
如果对象有多个构造子,对象之间又存在继承关系,事情就会变得特别讨厌。为了让所有东西都
正确地初始化,你必须将对子类构造子的调用转发给超类的构造子,然后处理自己的参数。这可
能造成构造子规模的进一步膨胀。
尽管有这些缺陷,但我仍然建议你首先考虑构造子注入。不过,一旦前面提到的问题真的成了问
题,你就应该准备转为使用设值方法注入。
代码配置 vs. 配置文件
另一个问题相对独立,但也经常与其他问题牵涉在一起:如何配置服务的组装,通过配置文件还
是直接编码组装?对于大多数需要在多处部署的应用程序来说,一个单独的配置文件会更合适。
配置文件几乎都是XML 文件,XML 也的确很适合这一用途。不过,有些时候直接在程序代码中
实现装配会更简单。譬如一个简单的应用程序,也没有很多部署上的变化,这时用几句代码来配
置就比XML 文件要清晰得多。
在Java 世界里,我们听到了来自配置文件的不和谐音——每个组件都有它自己的配置文件,而
且格式还各各不同。如果你要使用一打这样的组件,你就得维护一打的配置文件,那会很快让你
烦死。
在这里,我的建议是:始终提供一种标准的配置方式,使程序员能够通过同一个编程接口轻松地
完成配置工作。至于其他的配置文件,仅仅把它们当作一种可选的功能。借助这个编程接口,开
发者可以轻松地管理配置文件。如果你编写了一个组件,则可以由组件的使用者来选择如何管理
配置信息:使用你的编程接口、直接操作配置文件格式,或者定义他们自己的配置文件格式,并
将其与你的编程接口相结合。