1、- Page 1-IoC 容器和 Dependency Injection 模式 撰文/Martin Fowler 编译/透明 Java 社群近来掀起了一阵轻量级容器的热潮,这些容器能够帮助开发者将来自不同项目的组件 组装成为一个内聚的应用程序。在它们的背后有着同一个模式,这个模式决定了这些容器进行组 件装配的方式。人们用一个大而化之的名字来称呼这个模式:“控制反转”(Inversion of Control,IoC)。在本文中,我将深入探索这个模式的工作原理,给它一个更能描述其特点的名 字 “依赖注入”(Dependency Injection),并将其与“服务定位器”(Service Lo
2、cator ) 模式作一个比较。不过,这两者之间的差异并不太重要,更重要的是:应该将组件的配置与 用 开两个模式的目 是这个。 在 级Java 的 在一个有 的 :有 人 入 来 J2EE 的 自 ,这大 发 在open source 社群。在 大程 ,这 作是开发者 J2EE 的重和作的currency1应,其中的“有 fi的fl ,的“了一些 的方 。J2EE 开发者的一个”是组装不同的程序 : web 控制 器和 是不同的 开发的, 一 , 应该 它们配 工作 过 决这个”,有个 索 这个方 发 ,了更用的“组装 组件”的方 。这 的 称为“轻量级容器”,PicoContainer和 S
3、pring 在 中。 在这些容器背后,一些有 的 原发着作用。这些原 了特定容器的 , 了Java 的 。在本文中,我要 这些原。我 用的 是 Java ,我的大 文 一 ,这些原 同 用 别的OO 环境,特别是.NET。 组件和服务 “装配程序 ”,这 的话”立即将我拖进了一个棘手的 语”:区 “服务”(service) 和“组件”(component ) 毫不费 地找关 这两个词定义的长篇大论, 种 矛盾的定义会 感受我处的窘境。有鉴 , 这两个遭了严重滥用的词汇,我将首 先说明它们在本文中的用 。 谓“组件”是指这 一个软件单 :它将 作者 控制的其他应用程序 用,后者不能 组件进行修改
4、。 是说, 用一个组件的应用程序不能修改组件的源 , 过作者预 留的某种途径其进行扩 ,改变组件的行为。 服务和组件有某种相似之处:它们 将 外部的应用程序 用。在我来,两者之间最大的差异 在 :组件是在本地 用的(JAR 文件、程序集、DLL、或者源导入);而服务是要过 同 或异 的远程 来远程 用的( web service、消息统、RPC,或者 socket )。 在本文中,我将 要 用“服务”这个词,文中的大 逻辑 同 用 本地组件。实际 , 为了方便地访远程服务, 往往需要某种本地组件 。不过,“组件或者服务”这 一个词 组实在太麻烦了,而且“服务”这个词当下 行,本文将用“服务”指
5、 这两者。 - Page 2-一个简单的子 为了更好地说明”,我要引入一个子。和我前用的有子一 ,这是一个 级简单的 子:它非小,小得有点不够真实,足帮助 清其中的道理,而不 陷入真实子的泥 潭中 自拔。 在这个子中,我编写了一个组件,用 一份电影清单,清单 的影片 是一位特定 的导演执导的。实 这个伟大的功能只需要一个方 : class MovieLister. public Movie moviesDirectedBy(String arg) List allMovies = finder.findAll(); for (Iterator it = allMovies.iterator()
6、; it.hasNext();) Movie movie = (Movie) it.next(); if (!movie.getDirector().equals(arg) it.remove(); return (Movie) allMovies.toArray(new MovieallMovies.size(); ,这个功能的实 其简单:moviesDirectedBy 方 首先请求 finder (影片搜寻 者) (我们稍后会谈这个 )返currency1后者 道的有影片, 后遍历finder 返currency1 的清单,并返currency1其中特定的某个导演执导的影片。非简单,不过
7、不必担心,这只是整个子 的脚手 罢了。 我们真fl要考察的是finder ,或者说,将MovieLister 与特定的finder 连 起来。为什么我们这个”特别感兴 因为我 这个 的 moviesDirectedBy 方 不依赖 影片的实际 方式。,这个方 只能引用一个finder ,而finder 必 道findAll 方 作currency1应。为了帮助 者更清 地理 ,我给finder定义了 一个 : public interface MovieFinder List findAll(); 在,两个 之间 有什么 关。是,当我要实际寻找影片 ,必 MovieFinder 的某个 子 。
8、在这 ,我 “ 子 ”的 在 MovieLister 的 子中。 class MovieLister. private MovieFinder finder; public MovieLister() finder = new ColonDelimitedMovieFinder(“movies1.txt“); 这个实 的名字说明:我将要 一个 的文本文件中 得影片 。 不必 心 的实 ,只要 fl这 一个实 了。 这个 只我自 用,一 ”。是, 我的 服 这个 的功能, fl - Page 3-用我的程序,会么 他们 影片清单currency1 在一个 的文本文件中,并 且 这个文件名为“ m
9、ovie1.txt ”,么一 “是 ”。 他们只是给这个文件改改名, 我 一个配置文件 得文件名,这 容。是, 他们用 不同的方式 SQL 、XML 文件、web service,或者一种式的文本文件来 影片清单 在这种fifl下,我们需要用一个 来 。 定义了MovieFinder ,我 不用修改 moviesDirectedBy 方 。是,我 需要过某种途径 得 的MovieFinder 实 的实。 1 :“在 MovieLister 中 MovieFinder 实” 的依赖关 1 了这种fifl下的依赖关:MovieLister 依赖 MovieFinder , 依赖 的实 。我们当 M
10、ovieLister 只依赖 ,我们要 得一个 MovieFinder 子 的实 在 Patterns of Enterprise Application Architecture 一中,我们 这种fifl称为“件” (plugin):MovieFinder的实 不是在编译连入程序之中的,因为我并不 道我的 会 用个实 。我们 MovieLister 能够与 MovieFinder的”实 配 工作,并且 在行入 的实 ,入 作 我(原作者)的控制。这 的”是: 这个连过程, MovieLister 在不 道实 的前下与其实 同工作。 将这个子而之,在一个真实的统中,我们 能有个服务和组件。在”
11、 ,我们 用组件的fi ,过 与 的组件 ( 组件并 有 一个 , 过 配器与之 )。是, 我们 不同的方式部这个统,需要用件 制来处理服务之间的 过程,这 我们 能在不同的部方 中 用不同的实 。 , 在的心”是:将这些件组 成一个应用程序 这是 的轻量级容器 的 要”,而它们 决这个”的手 一外地是控制反转(Inversion of Control) 模式。 控制反转 位轻量级容器的作者 地我说:这些容器非有用,因为它们实 了“控制反转”。这 的说 我深感 :控制反转是 有的特 , 因为 用了控制反转 为这 些轻量级容器与 不同,好 在说“我的 是与 不同的,因为它有 个 子”。 ”的关在
12、 :它们反转了方 的控制 我 一 的控制反转 的是用 的 控 。的用 是 应用程序来控制的, 预先 一 ,“入名”、 - Page 4-“入地 ” ,应用程序 息,并 currency1用 的应。而在用 环境下, UI 将 执行一个 环, 的应用程序只需为 的 个区 件处理即 。 在这 ,程序的 控 发 了反转: 应用程序了 。 这些 的容器,它们反转的是“定位件的 实 ”。在前 个简单的子中, MovieLister 定位 MovieFinder 的 实 它实化后者的一个子 。这 一 来,MovieFinder 不成其为一个件了,因为它并不是在行入应用程序中的。而这 些轻量级容器 用了更为
13、的 ,只要件 一定的 ,一个独立的组装模块能够 将件的 实 “注射”应用程序中。 因,我fl我们需要给这个模式起一个更能说明其特点的名字“控制反转”这个名字太泛了, 人有些 。与 位 IoC 爱好者讨论之后,我们决定将这个模式叫做“依赖注入” (Dependency Injection)。 下 ,我将开始介绍 Dependency Injection 模式的种不同式。不过,在之前,我要首 先指:要消除应用程序件实 的依赖,依赖注入并不是唯一的, 用Service Locator模式 得同 的效 。介绍 Dependency Injection 模式之后,我 会谈Service Locator模
14、式。 依赖注入的种式 Dependency Injection 模式的基本思fl是:用一个单独的 (装配器)来 得MovieFinder 的一个 的实 ,并将其实赋给 MovieLister 的一个字 。这 一来,我们得了2 的依赖: 2 :引入依赖注入器之后的依赖关 依赖注入的式 要有三种,我 别将它们叫做 子注入(Constructor Injection)、 值 方 注入(Setter Injection)和 注入(Interface Injection)。 过最近关 IoC 的 一些讨论材料, 不难:这三种注入式 别是type 1 IoC ( 注入)、type 2 IoC ( 值方 注
15、入)和type 3 IoC ( 子注入)。我发 字编 往往比较难记,我 用了 这 的名方式。 - Page 5-用PicoContainer进行 子注入 首先,我要 者 用一个名为 PicoContainer 的轻量级容器 成依赖注入。之 这 开始, 要是因为我在 ThoughtWorks 公司的个同在 PicoContainer 的开发社群中 非 跃 错, 说是某种偏袒吧。 PicoContainer过 子来判断“将 MovieFinder实注入 MovieLister ”。因, MovieLister 必 声明一个 子,并在其中包含有需要注入的 : class MovieLister. p
16、ublic MovieLister(MovieFinder finder) this.finder = finder; MovieFinder 实本身 将 PicoContainer来管理,因文本文件的名字 容器注入: class ColonMovieFinder. public ColonMovieFinder(String filename) this.filename = filename; 随后,需要告诉 PicoContainer: 个 别与个实 关联、将个字符串注入 MovieFinder组件。 private MutablePicoContainer configureConta
17、iner() MutablePicoContainer pico = new DefaultPicoContainer(); Parameter finderParams = new ConstantParameter(“movies1.txt“); pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams); pico.registerComponentImplementation(MovieLister.class); return pico; 这 配置 位 一个
18、 。 我们这个子, 用我的 MovieLister 的 需要在 自 的 置 中编写 的配置 。当 ,“ 将这些配置 息 在一个单独的配置文件中, 这 是一种见的做 。 编写一个 来 配置文件, 后容器进行 的 置。尽管 PicoContainer 本身并不包含这项功能,一个与它关紧密的项目 NanoContainer 了一些包装,开发者 用XML 配置文件currency1 配置 息。NanoContainer能够 析XML 文 件,并底下的 PicoContainer进行配置。这个项目的哲学观念是:将配置文件的式与底 下的配置制 开。 用这个容器, 写的 大概会是这 : public voi
19、d testWithPico() MutablePicoContainer pico = configureContainer(); MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class); Movie movies = lister.moviesDirectedBy(“Sergio Leone“); - Page 6-assertEquals(“Once Upon a Time in the West“, movies0.getTitle(); 尽管在这 我 用了 子注入,实际 PicoC
20、ontainer 支持 值方 注入,不过该项目的 开发者更荐 用 子注入。 用 Spring进行 值方 注入 Spring 是一个用途泛的 级Java 开发 ,其中包括了 务、持久化 、web 应用开发和 JDBC 用功能的 。和 PicoContainer一 ,它 同 支持 子注入和 值方 注入,该项目的开发者更荐 用 值方 注入恰好 这个子。 为了 MovieLister 受注入,我需要为它定义一个 值方 ,该方 受 型为 MovieFinder的参: class MovieLister. private MovieFinder finder; public void setFinder(
21、MovieFinder finder) this.finder = finder; 似地,在MovieFinder的实 中,我 定义了一个 值方 ,受 型为 String 的参: class ColonMovieFinder. public void setFilename(String filename) this.filename = filename; 三 是 定配置文件。Spring 支持 种配置方式, 过 XML 文件进行配置, 在 中配置。不过,XML 文件是比较理fl的配置方式。 movies1.txt 是,测 大概像下 这 : public void testWithSprin
22、g() throws Exception ApplicationContext ctx = new - Page 7-FileSystemXmlApplicationContext(“spring.xml“); MovieLister lister = (MovieLister) ctx.getBean(“MovieLister“); Movie movies = lister.moviesDirectedBy(“Sergio Leone“); assertEquals(“Once Upon a Time in the West“, movies0.getTitle(); 注入 除了前 两种注
23、入 ,“ 在 中定义需要注入的 息,并过 成注入。Avalon 用了 似的 。在这 ,我首先用简单的 说明它的用 ,后 “会有更深入 的讨论。 首先,我需要定义一个 ,组件的注入将过这个 进行。在本中,这个 的用途是将 一个 MovieFinder实注入继承了该 的 。 public interface InjectFinder void injectFinder(MovieFinder finder); 这个 应该 MovieFinder 的人一并。”fl要 用 MovieFinder 实的 (MovieLister ) 必 实 这个 。 class MovieLister implemen
24、ts InjectFinder. public void injectFinder(MovieFinder finder) this.finder = finder; 后,我 用 似的方 将文件名注入MovieFinder的实 : public interface InjectFilename void injectFilename (String filename); class ColonMovieFinder implements MovieFinder, InjectFilename public void injectFilename(String filename) this.fi
25、lename = filename; 在,“需要用一些配置 将有的组件实 装配起来。简单起见,我在 中 成配置, 并将配置好的 MovieLister currency1 在名为 lister的字 中: class IfaceTester. private MovieLister lister; private void configureLister() ColonMovieFinder finder = new ColonMovieFinder(); finder.injectFilename(“movies1.txt“); lister = new MovieLister(); list
26、er.injectFinder(finder); - Page 8-测 用这个字 : class IfaceTester. public void testIface() configureLister(); Movie movies = lister.moviesDirectedBy(“Sergio Leone“); assertEquals(“Once Upon a Time in the West“, movies0.getTitle(); 用 Service Locator 依赖注入的最大好处在 :它消除了MovieLister MovieFinder实 的依赖。这 一来,我 Movi
27、eLister 给 , 他们根自 的环境入一个 的 MovieFinder实 即 。不过,Dependency Injection 模式并不是打破这依赖关的唯一 手 ,一种方 是 用Service Locator模式。 Service Locator模式背后的基本思fl是:有一个 (即服务定位器) 道 得一个应用 程序需的有服务。 是说,在我们的子中,服务定位器应该有一个方 ,用 得一个 MovieFinder 实。当 ,这不过是 麻烦换了一个 子,我们 必 在 MovieLister 中 得服务定位器,最终得的依赖关 3 : 3 : 用Service Locator 模式之后的依赖关 在这
28、,我 ServiceLocator 实 为一个 Singleton 的注册 , 是 MovieLister 在实化 过ServiceLocator 得一个 MovieFinder实。 class MovieLister. MovieFinder finder = ServiceLocator.movieFinder(); class ServiceLocator. - Page 9-public static MovieFinder movieFinder() return soleInstance.movieFinder; private static ServiceLocator sole
29、Instance; private MovieFinder movieFinder; 和注入的方式一 ,我们 必 服务定位器配置。在这 ,我在 中进行配置, 一种过配置文件 得的制 并非难。 class Tester. private void configure() ServiceLocator.load(new ServiceLocator(new ColonMovieFinder(“movies1.txt“); class ServiceLocator. public static void load(ServiceLocator arg) soleInstance = arg; pub
30、lic ServiceLocator(MovieFinder movieFinder) this.movieFinder = movieFinder; 下 是测 : class Tester. public void testSimple() configure(); MovieLister lister = new MovieLister(); Movie movies = lister.moviesDirectedBy(“Sergio Leone“); assertEquals(“Once Upon a Time in the West“, movies0.getTitle(); 我 听这
31、 的论调:这 的服务定位器不是什么好东西,因为 换它返currency1的服务实 , 而导致 它们进行测 。当 , 的 糟糕, 的“会这 的麻烦; 良好的 。在这个子中,ServiceLocator 实 是一个简单的容器,只需 要它做一些简单的修改, 它返currency1用 测 的服务实 。 更的fifl,我 ServiceLocator派 个子 ,并将子 型的实传递给注册 的 变量。外,我 修改ServiceLocator 的静态方 , 其调用ServiceLocator 实 的方 ,而不是访实变量。我“ 用特定 线程的 制, 而特定 线程 的服务定位器。有这一 改进 修改ServiceL
32、ocator 的 用者。 一种改进的思路是:服务定位器 是一个注册 ,不是Singleton。Singleton 的“是实 注册 的一种简单途径,这只是一个实 的决定, 轻松地改变它。 - Page 10-为定位器 的 这种简单的实 方式有一个”:MovieLister 将依赖 整个ServiceLocator ,它 需要 用的却只是后者的一项服务。我们 这项服务一个单独的 ,减少 MovieLister ServiceLocator 的依赖程 。这 一来,MovieLister 不必 用整个的 ServiceLocator ,只需声明它fl要 用的部 。 ,MovieLister 的者 应该
33、一并一个定位器 , 用者 过这个 得 MovieFinder实。 public interface MovieFinderLocator public MovieFinder movieFinder(); 真实的服务定位器需要实 述 ,访 MovieFinder实的能 : MovieFinderLocator locator = ServiceLocator.locator(); MovieFinder finder = locator.movieFinder(); public static ServiceLocator locator() return soleInstance; publ
34、ic MovieFinder movieFinder() return movieFinder; private static ServiceLocator soleInstance; private MovieFinder movieFinder; 应该 注fi了: fl要 用 ,我们不能再过静态方 访服务我们必 首先过ServiceLocator 得定位器实, 后 用定位器实得我们fl要的服务。 态服务定位器 是一个静态定位器的子 需要的每项服务,ServiceLocator 有应的方 。这并不是实 服务定位器的唯一方式, 一个 态服务定位器, 在其中注 册需要的”服务,并在行决定 得一项
35、服务。 在本中,ServiceLocator 用一个 map 来currency1 服务 息,而不再是将这些 息currency1 在字 中。外,ServiceLocator“了一个用的方 ,用 和载服务 。 class ServiceLocator. private static ServiceLocator soleInstance; public static void load(ServiceLocator arg) soleInstance = arg; private Map services = new HashMap(); public static Object getSer
36、vice(String key) return soleInstance.services.get(key); public void loadService (String key, Object service) - Page 11-services.put(key, service); 同 需要服务定位器进行配置,将服务 与 当的关字载定位器中: class Tester. private void configure() ServiceLocator locator = new ServiceLocator(); locator.loadService(“MovieFinder“, n
37、ew ColonMovieFinder(“movies1.txt“); ServiceLocator.load(locator); 我 用与服务 名称相同的字符串作为服务 的关字: class MovieLister. MovieFinder finder = (MovieFinder) ServiceLocator.getService(“MovieFinder“); 而言,我不喜欢这种方式。 疑,这 实 的服务定位器 有更强的 ,它的 用方 式不够观明朗。我只有过文本式的关字能找一个服务 。相比之下,我更欣赏“ 过一个方 明“ 得服务 ”的方式,因为这 用者能够 定义中清 地 道 得 某项
38、服务。 用Avalon兼顾服务定位器和依赖注入 Dependency Injection 和 Service Locator两个模式并不是 斥的, 同 用它们, Avalon 是这 的一个子。Avalon 用了服务定位器,“ 得定位器”的 息 是过注入的方式告 组件的。 前 一 用的子,Berin Loritsch发送给了我一个简单的Avalon 实 版本: public class MyMovieLister implements MovieLister, Serviceable private MovieFinder finder; public void service( Service
39、Manager manager ) throws ServiceException finder = (MovieFinder)manager.lookup(“finder“); service 方 是 注入的子,它 容器 将一个 ServiceManager 注入 MyMovieLister 。ServiceManager是一个服务定位器。在这个子中,MyMovieLister 并不 ServiceManager currency1 在字 中,而是马 借助它找 MovieFinder 实,并将 后者currency1 起来。 作一个 在为止,我一在阐述自 这两个模式(Dependency I
40、njection 模式和 Service - Page 12-Locator模式) 它们的变化式的 。 在,我要开始讨论他们的优点和缺点,便指 它们 自 用的场景。 Service Locator vs. Dependency Injection 首先,我们 Service Locator和 Dependency Injection 之间的。应该注fi,尽管我 们前 个简单的子不足 来,实际 这两个模式 了基本的 能 论 用个模式,应用程序 不依赖 服务 的 实 。两者之间最重要的区别在 :这 个“ 实 ”什么方式给应用程序 。 用 Service Locator 模式 ,应用程序 服务定位器
41、发送一个消息,明“要求服务的实 ; 用 Dependency Injection 模式 ,应用程序 不发显式的请求,服务的实 自 会 在应用程序 中,这 是谓 “控制反转” 控制反转是 的 同特 ,它 要求 付一定的 价:它会增理 的难 ,并且给调 带来一定的困难。,整来说,除非必要,否我会尽量避免 用它。这并不fi味着控制反 转不好,只是我 为在 用一个更为观的方 ( Service Locator 模式)会比 较 。 一个关的区别在 : 用 Service Locator 模式 ,服务的 用者必 依赖 服务定位器。 定位器 隐藏 用者服务 实 的依赖, 必 首先定位器本身。,”的 明朗了:
42、 Service Locator“是 Dependency Injection, 决 “定位器的依 赖”是否会给 带来麻烦。 Dependency Injection 模式 帮助 清组件之间的依赖关: 只需观察依赖注入的 制( 子), 整个依赖关。而 用Service Locator模式 , 必 在源 中处搜索服务定位器的调用。 文 索能 的 IDE 简化这一工作,“ 是不观察 子或者 值方 来得轻松。 这个 要 决 服务 用者的 。 的应用程序中有 不同的 要 用一个服务, 么应用程序 服务定位器的依赖不是什么大”。在前 的子中,我要 MovieLister 给 用,这种fifl下 用服务定
43、位器 好:我的 们只需要定位器 做一点配置(过配置文件或者某些配置 的 ), 其 的服务实 了。在这 种fifl下,我不 Dependency Injection 模式的控制反转有什么 引人的地方。 是, MovieLister 作一个组件,要将它给别人写的应用程序 用,fifl不 同了。在这种 ,我 预测 用者会 用什么 的服务定位器API,每个 用者 能有自 的服务定位器,而且之间 兼容。一种 决 是为每项服务单独的 , 用者 编写一个 配器, 我的 与他们的服务定位器相配 。即便,我 需要 一 个服务定位器中寻找我 定的 。而且一 用 了 配器,服务定位器的简单 大 大 了。 一方 ,
44、用 Dependency Injection 模式,组件与注入器之间不会有依赖关,因 组件 注入器 得更 的服务,只能 得配置 息中的些。这 是 Dependency Injection 模式的 之一。 人们 用 Dependency Injection 模式的一个见理是:它简化了测 工作。这 的 关是: 测 的需要, 必 能够轻松地在“真实的服务实 ”与“测 用的 组件” 之间 换。是, 单 这个 来考 ,Dependency Injection 模式和Service Locator - Page 13-模式其实并 有太大区别:两者 能够 好地支持“ ”组件的入。之 人有 “Dependency Injection 模式更 测 ”的 ,我 是因为他们并 有 currency1 服务定 位器的 换 。