为什么需要了解控制反转(IoC、依赖注入(DI)以及它们的基本原理?对于这个问题,仁者见仁智者见智。如果你在知名的问答网站programmers.stackexchange.com上问这个问题,肯定会得到很多不同的答案!
你可能只是刚开始使用不同的DI框架并学习网上的示例,但如果你能够掌握对象关系映射(Object Relational Mapping,ORM)框架,比如Hibernate,你就可以变成编程高手。
本节首先介绍核心术语IoC和DI背后的一些原理,并探讨使用这一范式的好处。为了让这些概念不至于那么抽象,我们会以HollywoodService
为例展示它的转变过程——从自己构造依赖项变成被注入依赖项。
我们先从IoC开始,这个术语经常被(错误地)和DI互换使用。1
1 从字面上来看,IoC是指一种机制,使用这种机制的用例很多,实现方式也很多。DI只是其中一种具体用例的具体实现方式。但因为DI非常流行,所以人们经常误以为IoC就是DI,并且认为DI这种叫法比IoC更贴切。这是来自stackoverflow的更全面解释(英文):http://stackoverflow.com/questions/6550700/inversion-of-control-vs-dependency-injection。——译者注
3.1.1 控制反转
在使用非IoC范式编程时,程序逻辑的流程通常是由一个功能中心来控制的。如果设计得好,这个功能中心会调用各个可重用对象中的方法执行特定的功能。
使用IoC,这个“中心控制”的设计原则会被反转过来。调用者的代码处理程序的执行顺序,而程序逻辑则被封装在接受调用的子流程中。
IoC也被称为好莱坞原则,其思想可以归结为会有另一段代码拥有最初的控制线程,并且由它来调用你的代码,而不是由你的代码调用它。
好莱坞原则——“不要给我们打电话,我们会打给你”
好莱坞经纪人总是给人打电话,而不让别人打给他们!如果你曾经跟好莱坞经纪人提议,在明年夏天筹划一个“让Java程序员成为拯救世界的英雄”的大片,你也许会深谙其道。
换一种方式来看IoC,回想一下视频游戏Zork(http://en.wikipedia.org/wiki/Zork)用户界面的发展过程,从早期由命令行中的文本控制到如今用图形用户界面控制。
在命令行版本中,用户界面只有一个空白提示符,等着用户输入。当用户输入“向东”或者“Grue,快逃”的指令后,游戏的主应用逻辑会调用恰当的事件处理器来处理这些指令,并返回结果。这里的关键点是应用逻辑要控制调用哪个事件处理器。
而在GUI版本中,IoC开始发挥作用。由GUI框架来控制调用事件处理器,而不是由应用逻辑。当用户点击了一个动作,比如“向东”时,GUI框架会直接调用对应的事件处理器,而应用逻辑可以把重点放在处理动作上。
程序的主控被反转了,将控制权从应用逻辑中转移到GUI框架。1
1 程序中出现了专门用来实现调用和控制逻辑的GUI框架,应用逻辑中的代码只需关注应用请求的处理。——译者注
IoC有几种不同的实现,包括工厂模式、服务定位器模式,当然,还有依赖注入。这一术语最初由Martin Fowler在“控制反转容器和依赖注入模式”2中提出,然后迅速传遍大街小巷,反响强烈。
2 在Martin Fowler的网站http://martinfowler.com/中搜索Dependency Injection,你就可以找到这篇文章。
3.1.2 依赖注入
依赖注入是IoC的一种特定形态,是指寻找依赖项的过程不在当前执行代码的直接控制之下。虽然你也可以自己编写代码实现依赖注入机制,但大多数开发人员都会使用自带IoC容器的第三方DI框架,比如Guice。
注意 可以把IoC容器看做运行时环境。Java中为依赖注入提供的容器有Guice、Spring和PicoContainer。
IoC容器可以提供实用的服务,比如确保一个可重用的依赖项会被配置成单例模式。我们在3.3节介绍Guice时会探讨它的一些服务。
提示 把依赖项注入对象的方法有很多种。可以用专门的DI框架,但也可以不这么做!显式地创建对象实例(依赖项)并把它们传入对象中也可以和框架注入做的一样好。1
1 感谢Thiago Arrais(http://stackoverflow.com/users/17801/thiago-arrais)提供了这个提示。
与很多编程范式一样,理解使用DI的原因很重要。我们在表3-1中总结了它的主要好处。
表3-1 DI的好处
HollywoodService
对象不再需要创建它所需的SpreadsheetAgentFinder
对象,而是使用从外部传入的对象。如果面向接口编程,HollywoodService
可以接受任何类型的AgentFinder
传入易测性作为松耦合的延伸,还有个特殊的用例值得一提。为了测试,可以把测试替身2作为依赖项注入到对象中你可以注入一个总是返回相同价格的虚设票价服务,而不是使用“真实”的价格服务,因为它是外部服务,而且有时无法访问更强的内聚性你的代码可以专注于执行自己的任务,不用为了载入和配置依赖项而分心。这样还能增强代码的可读性你的DAO对象可以专注于查询工作,不用考虑JDBC驱动的细节可重用组件作为松耦合的延伸,你的代码应用范围会更加宽广,那些可以提供自己特定实现的用户都可以使用你的代码一个积极进取的软件开发人员可能会卖给你一个LinkedIn代理人查找器更轻盈的代码你的代码不再需要跨层传递依赖项,而是可以在需要依赖项的地方直接注入你不再需要把JDBC驱动的细节信息从service类往下传递,而是直接在真正需要它的DAO中直接注入这个驱动实例2 第11章会详细介绍测试替身。
把普通代码改写成依赖注入的代码是掌握这些理论的最佳方法,所以我们进入下一节吧。
3.1.3 转成DI
本节会重点介绍如何把不用IoC的代码变成使用工厂(或服务定位器)模式的代码,再变成使用DI的代码。在这些转变之后有一个共同的关键技术,即面向接口编程。使用面向接口编程,甚至可以在运行时更换对象。
注意 本节的目的是巩固你对DI的理解。因此某些比较套路化的代码被省略掉了。
假设你刚接手了一个小项目,要找出所有对Java开发人员比较友善的好莱坞经纪人。在下面的代码中,AgentFinder
接口有两个实现类:SpreadSheetAgentFinder
和WebServiceAgentFinder
。
代码清单3-1 接口AgentFinder
及其实现类
public interface AgentFinder{ public List<Agent> findAllAgents;}public class SpreadsheetAgentFinder implements AgentFinder{ @Override public List<Agent> findAllAgents { .. } //很多实现细节}public class WebServiceAgentFinder implements AgentFinder{ @Override public List<Agent> findAllAgents { .. } //很多实现细节}
为了使用经纪人查找器,项目中有个默认的HollywoodServic
类,它会从SpreadsheetAgentFinder
里得到一个经纪人列表,并根据是否友善对他们进行过滤,最终返回友善的经纪人列表。如下面的代码所示。
代码清单3-2 HollywoodService
——自己创建AgentFinder
的具体实现类实例
public class HollywoodService{ public static List<Agent> getFriendlyAgents { AgentFinder finder = new SpreadsheetAgentFinder;//①使用SpreadsheetAgentFinder List<Agent> agents = finder.findAllAgents;//调用接口方法 List<Agent> friendlyAgents = filterAgents(agents, /"Java Developers/"); return friendlyAgents;//返回友善的经纪人 }public static List<Agent> filterAgents(List<Agent> agents, String agentType){ List<Agent> filteredAgents = new ArrayList<>; for (Agent agent:agents) { if (agent.getType.equals(/"Java Developers/")) { filteredAgents.add(agent); } } return filteredAgents; }}
再看一遍上面代码里的HollywoodService
,注意到了吗?它被SpreadsheetAgentFinder
这个AgentFinder
的具体实现死死地黏上了①。
过去这种黏糊糊的实现对Java开发者来说司空见惯,不胜其烦!为了解决这些共性问题,设计模式应运而生。一开始,很多开发人员用工厂模式和服务定位器模式的各种变体来解决这类问题,它们全都是IoC的一种。
1. 使用工厂和/或服务定位器模式的HollywoodService
抽象工厂、工厂方法或服务定位器模式中的某个(或它们的某种组合)通常用来解决这种被依赖项黏上的问题。
注意 工厂方法和抽象工厂模式在Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides写的《设计模式:可复用面向对象软件的基础》(Addison-Wesley Professional,1994)中有所论述。服务定位器模式出现在Deepak Alur、John Crupi和Dan Malks编写的《J2EE核心模式》第二版(Prentice Hall,2003)中。
下面这个版本的HollywoodService
类用AgentFinderFactory
实现对AgentFinder
的动态选择。
代码清单3-3 HollywoodService
由工厂负责提供AgentFinder
public class HollywoodServiceWithFactory { public List<Agent> getFriendlyAgents(String agentFinderType)//①注入agentFinderType { /**②通过工厂实例获取AgentFinder*/ AgentFinderFactory factory = AgentFinderFactory.getInstance; AgentFinder finder = factory.getAgentFinder(agentFinderType); List<Agent> agents = finder.findAllAgents; List<Agent> friendlyAgents = filterAgents(agents, /"Java Developers/"); return friendlyAgents; } public static List<Agent> filterAgents(List<Agent> agents, String agentType) { ...//与代码清单3-2中的实现一样 }}
如你所见,现在没有特定的AgentFinder
实现类来黏你了。注入agentFinderType
①,让AgentFinderFactory
根据这一类型挑选令人满意的AgentFinder
供你享用②。
这和DI相当接近了,但还有两个问题。
- 代码中注入的是一个引用凭据(
agentFinderType
),而不是真正实现AgentFinder
的对象。 - 方法
getFriendlyAgents
中还有获取其依赖项的代码,达不到只关注自身职能的理想状态。
随着开发人员编写更清晰代码的意愿不断增强,DI技术也越来越流行,正在逐步取代工厂和服务定位器模式。
2. 使用DI的HollywoodService
你可能已经猜出接下来重构代码该做什么了!下一步要让AgentFinder
直接提供所需的getFriendlyAgents
方法。代码如下所示:
代码清单3-4 HollywoodService
——纯手工DI注入AgentFinder
public class HollywoodServiceWithDI{ public static List<Agent> emailFriendlyAgents(AgentFinder finder)//①注入AgentFinder { /**②执行查找逻辑*/ List<Agent> agents = finder.findAllAgents; List<Agent> friendlyAgents = filterAgents(agents, /"Java Developers/"); return friendlyAgents; } public static List<Agent> filterAgents(List<Agent> agents, String agentType) { ...//参见代码清单3-2 }}
看看这个纯手工打造的DI方案,AgentFinder
被直接注入到getFriendlyAgents
方法中①。现在这个getFriendlyAgents
方法干净利落,只专注于纯业务逻辑②。
不过这种手工打造DI的生产方式还是有让人头疼的地方。如何配置AgentFinder
具体实现的问题并没有解决,原本AgentFinderFactory
要做的工作还是要找个地方完成。
所以,能够真正让我们脱离苦海的只有自带IoC容器的DI框架。打个比方,DI框架就是把你的代码包起来的运行时环境,在你需要时为你注入依赖项。
DI框架的优势在于它可以随时随地为你的代码提供依赖项。因为框架中有IoC容器,在运行时,你的代码需要的所有依赖项都会在那里准备好。
如果HollywoodService
类使用标准的JSR-330注解(可以使用任何与JSR-330兼容的框架),那么它会是什么样子?
3. 使用JSR-330 DI的HollywoodService
来看看这个例子的最终版本,用框架来执行DI操作。在这里,DI框架用标准的JSR-330@Inject
注解将依赖项直接注入到getFriendlyAgents
方法中,代码如下所示:
代码清单3-5 HollywoodService
——用JSR-330 DI注入AgentFinder
public class HollywoodServiceJSR330{ @Inject public static List<Agent> emailFriendlyAgents(AgentFinder finder)//①JSR-330注入AgentFinder { /**执行查找逻辑*/ List<Agent> agents = finder.findAllAgent; List<Agent> friendlyAgents = filterAgents(agents, /"Java Developers/"); return friendlyAgents; } public static List<Agent> filterAgents(List<Agent> agents, String agentType) { ...//参见代码清单3-2 }}
现在AgentFinder
的某个具体实现(比如WebServiceAgentFinder
)类的实例是由支持JSR-330@Inject
注释的DI框架在运行时注入的①。
提示 尽管JSR-330注解可以在方法上注入依赖项,但通常只用于构造方法或set方法中。下一节会对这一规范做进一步探讨。
让我们结合代码清单3-5中的HollywoodServiceJSR330
类再来重温一下依赖注入对我们的帮助。
松耦合——
HollywoodService
不再依赖于AgentFinder
的具体类来完成工作。可测性——为了测试
HollywoodService
类,你可以注入一个返回固定数量经纪人的基本Java类(比如POJOAgentFinder
),这在测试驱动的开发中被称为存根类。这对于单元测试来说非常完美,因为你不再需要Web服务、电子表格或其他第三方实现之类的东西。更强的内聚性——你的代码不用再和工厂打交道,不用四下里去抓依赖项,只需要执行业务逻辑。
可重用的组件——假如有个开发人员想用你的API,现在需要注入一个他们定制的
AgentFinder
实现类,就说JDBCAgentFinder
吧,想象一下他轻松惬意的表情吧。更轻盈的代码——
HollywoodServiceJSR330
中的代码量与最初的HollywoodService
相比明显减少了很多。
使用DI正在逐步成为优秀Java开发人员的标准实践,几个流行的容器都提供了优异的DI能力。然而就在不久之前,DI框架领域还是群雄割据,它们风格迥异,各行其是,使用IoC容器的配置标准都自成体系。即便遵循类似配置风格的框架(比如XML或Java注解),还是存在什么是共通的注解和配置这个问题。
新的DI标准化方式(JSR-330)就是要解决这个问题。它对大多数Java DI框架的核心能力做了很好的汇总。因为有DI框架(比如Guice)的内部工作机制作为其坚实的基础,所以接下来我们会深入探讨这一标准化方式。