PicoContainer
英文原文:Inversion of Control Containers and the Dependency Injection pattern
Java社群迩来掀起了一阵轻量级容器的高潮,这些容器可以协助开辟者未来自分歧项目标组件组装成为一个内聚的使用顺序。在它们的面前有着统一个形式,这个形式决议了这些容器实行组件拆卸的方法。人们用一个大而化之的名字来称谓这个形式:“把持反转”( Inversion of Control,IoC)。在本文中,我将深化探究这个形式的任务道理,给它一个更能描绘其特色的名字——“依靠注入”(Dependency Injection),并将其与“办事定位器”(Service Locator)形式作一个比拟。不外,这二者之间的差别其实不太主要,更主要的是:应当将组件的设置装备摆设与运用别离开——两个形式的目的都是这个。
在企业级Java的天下里存在一个风趣的景象:有非常多人投入非常多精神来研讨主流J2EE 技巧的替换品——天然,这大多发作在open source社群。在很大水平上,这可以看做是开辟者对主流J2EE技巧的笨重和庞杂作出的回应,但此中确实有非常多极富创意的设法,确实供给了一些可供选择的计划。J2EE开辟者常碰到的一个问题就是怎么组装分歧的顺序元素:假如web把持器系统构造和数据库接口是由分歧的团队所开辟的,相互简直全无所闻,你应当怎么让它们共同任务?非常多框架测验考试过处理这个问题,有几个框架索性朝这个标的目的开展,供给了更通用的“组装各层组件”的计划。如许的框架凡是被称为“轻量级容器”,PicoContainer和Spring都在此列中。
在这些容器面前,一些风趣的设计准绳发扬着感化。这些准绳曾经逾越了特定容器的范围,乃至曾经逾越了Java平台的范围。在本文中,我就要开端提醒这些准绳。我运用的典范是Java代码,但正如我的大大多数文章一样,这些准绳也异样实用于此外面向工具的情况,特殊是.NET。
拆卸顺序元素,如许的话题立刻将我拖进了一个顺手的术语问题:怎么辨别“办事”(service)和“组件”(component)?你可以绝不吃力地找出关于这两个词界说的长篇大论,各类相互抵触的界说会让你感触感染到我所处的困境。有鉴于此,关于这两个遭到了严重滥用的辞汇,我将起首阐明它们在本文中的用法。
所谓“组件”是指如许一个软件单位:它将被作者没法把持的其他使用顺序运用,但后者不克不及对组件实行修正。也就是说,运用一个组件的使用顺序不克不及修正组件的源代码,但可以经过作者预留的某种道路对其实行扩大,以改动组件的行动。
办事和组件有某种类似的地方:它们都将被内部的使用顺序运用。在我看来,二者之间最大的差别在于:组件是在当地运用的(例如JAR文件、顺序集、DLL、或许源码导入);而办事是要经过同步或异步的远程接口来远程运用的(例如web service、音讯零碎、RPC,或许socket)。
在本文中,我将首要运用“办事”这个词,但文中的大大多数逻辑也异样实用于当地组件。实践上,为了便利地拜访远程办事,你常常需求某种当地组件框架。不外,“组件或许办事”如许一个词组真实太费事了,并且“办事”这个词当下也很盛行,所以本文将用“办事”指代这二者。
为了更好地阐明问题,我要引入一个例子。和我之前用的一切例子一样,这是一个超等容易的例子:它十分小,小得有点不敷真实,但足以协助你看清此中的事理,而不至于堕入真实例子的泥潭中没法自拔。
在这个例子中,我编写了一个组件,用于供给一份片子清单,清单上列出的影片都是由一名特定的导演执导的。完成这个巨大的功用只需求一个办法:
class MovieLister... public Movie[] moviesDirectedBy(String arg) { List allMovies = finder.findAll(); for (Iterator it = allMovies.iterator(); it.hasNext();) { Movie movie = (Movie) it.next(); if (!movie.getDirector().equals(arg)) it.remove(); } return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]); }
你可以看到,这个功用的完成极端容易:moviesDirectedBy办法起首恳求finder(影片搜索者)工具(我们稍后谈判到这个工具)前往后者所晓得的一切影片,然后遍历finder工具前往的清单,并前往此中由特定的某个导演执导的影片。十分容易,不外不用担忧,这只是全部例子的脚手架而已。
我们真正想要调查的是finder工具,或许说,怎么将MovieLister工具与特定的finder工具衔接起来。为何我们对这个问题特殊感兴味?由于我盼望上面这个美丽的moviesDirectedBy办法完整不依靠于影片的实践存储方法。所以,这个办法只能援用一个finder工具,而finder工具则必需晓得怎么对findAll 办法作出回应。为了协助读者更明白天文解,我给finder界说了一个接口:
public interface MovieFinder { List findAll(); }
如今,两个工具之间没有甚么耦合关系。可是,当我要实践寻觅影片时,就必需触及到MovieFinder的某个详细子类。在这里,我把触及详细子类的代码放在MovieLister类的结构函数中。
class MovieLister... private MovieFinder finder; public MovieLister() { finder = new ColonDelimitedMovieFinder("movies1.txt"); }
这个完成类的名字就阐明:我将要从一个逗号分开的文本文件中取得影片列表。你不用费心详细的完成细节,只需想象如许一个完成类就能够了。
假如这个类只由我本人运用,一切都没问题。可是,假如我的冤家叹服于这个出色的功用,也想运用我的顺序,那又会怎样呢?假如他们也把影片清单保管在一个逗号分开的文本文件中,而且也把这个文件定名为“ movie1.txt ”,那末一切仍是没问题。假如他们只是给这个文件改更名,我也能够从一个设置装备摆设文件取得文件名,这也很轻易。可是,假如他们用完整分歧的方法——例如SQL 数据库、XML 文件、web service,或许另外一种格局的文本文件——来存储影片清单呢?在这类状况下,我们需求用另外一个类来获得数据。因为曾经界说了MovieFinder接口,我可以不必修正moviesDirectedBy办法。可是,我依然需求经过某种道路取得适宜的MovieFinder完成类的实例。
图1展示了这类状况下的依靠关系:MovieLister类既依靠于MovieFinder接口,也依靠于详细的完成类。我们固然盼望MovieLister类只依靠于接口,但我们要怎么取得一个MovieFinder子类的实例呢?
在 P of EAA(Patterns of Enterprise Application Architecture)一书中,我们把这类状况称为插件(plugin):MovieFinder的完成类不是在编译期连入顺序当中的,由于我其实不晓得我的冤家会运用哪一个完成类。我们盼望MovieLister类可以与MovieFinder的任何完成类共同任务,而且答应在运转期拔出详细的完成类,拔出举措完整离开我的把持。这里的问题就是:怎么设计这个衔接进程,使MovieLister类在不晓得完成类细节的条件下与实在例协同任务。
将这个例子推而广之,在一个真实的零碎中,我们可能有数十个办事和组件。在任什么时候候,我们总可以对运用组件的情况加以笼统,经过接口与详细的组件交换(假如组件并没有设计一个接口,也能够经过适配器与之交换)。可是,假如我们盼望以分歧的方法安排这个零碎,就需求用插件机制来处置办事之间的交互进程,如许我们才可能在分歧的安排计划中运用分歧的完成。
所以,如今的中心问题就是:怎么将这些插件组分解一个使用顺序?这恰是重生的轻量级容器所面对的首要问题,而它们处理这个问题的手腕无一破例地是把持反转(Inversion of Control)形式。
几位轻量级容器的作者曾自豪地对我说:这些容器十分有效,由于它们完成了把持反转。如许的说推让我深感困惑:把持反转是框架所共有的特点,假如仅仅由于运用了把持反转就以为这些轻量级容器异乎寻常,就好象在说我的轿车是异乎寻常的,由于它有四个轮子。
问题的要害在于:它们反转了哪方面的把持?我第一次接触到的把持反转针对的是用户界面的主控权。早期的用户界面是完整由使用顺序来把持的,你预先设计一系列号令,例如输出姓名、输出地址等,使用顺序逐条输出提醒信息,并取回用户的呼应。而在图形用户界面情况下,UI框架将担任履行一个主轮回,你的使用顺序只需为屏幕的各个区域供给事情处置函数便可。在这里,顺序的主控权发作了反转:从使用顺序移到了框架。
关于这些重生的容器,它们反转的是怎么定位插件的详细完成。在后面阿谁容易的例子中,MovieLister类担任定位MovieFinder的详细完成——它间接实例化后者的一个子类。如许一来,MovieFinder也就不成其为一个插件了,由于它并非在运转期拔出使用顺序中的。而这些轻量级容器则运用了更加灵敏的方法,只需插件遵照必定的规矩,一个自力的组装模块就可以够将插件的详细完成打针到使用顺序中。
因而,我想我们需求给这个形式起一个更能阐明其特色的名字——“把持反转”这个名字太泛了,经常让人有点困惑。与多位IoC 喜好者会商以后,我们决议将这个形式叫做“依靠注入”(Dependency Injection)。
下面,我将开端引见Dependency Injection形式的几种分歧方式。不外,在此之前,我要起首指出:要消弭使用顺序对插件完成的依靠,依靠注入并非独一的选择,你也能够用Service Locator形式取得异样的后果。引见完Dependency Injection形式以后,我也谈判到Service Locator 形式。
Dependency Injection 形式的根本思惟是:用一个独自的工具(拆卸器)来取得MovieFinder的一个适宜的完成,并将实在例赋给MovieLister类的一个字段。如许一来,我们就失掉了图2所示的依靠图。
图2:引入依靠注入器以后的依靠关系
依靠注入的方式首要有三种,我辨别将它们叫做结构函数注入(Constructor Injection)、设值办法注入(Setter Injection)和接口注入(Interface Injection)。假如读过比来关于IoC的一些会商资料,你不好看出:这三种注入方式辨别就是type 1 IoC(接口注入)、type 2 IoC(设值办法注入)和type 3 IoC(结构函数注入)。我发明数字编号常常比拟难记,所以我运用了这里的定名方法。
起首,我要向读者展现怎么用一个名为PicoContainer的轻量级容器完成依靠注入。之所以从这里开端,首要是由于我在ThoughtWorks公司的几个同事在PicoContainer的开辟社群中十分活泼——没错,也能够说是某种左袒吧。
PicoContainer经过结构函数来判别怎么将MovieFinder实例注入MovieLister 类。因而,MovieLister类必需声明一个结构函数,并在此中包括一切需求注入的元素:
class MovieLister... public MovieLister(MovieFinder finder) { this.finder = finder; }
MovieFinder实例自身也将由PicoContainer来治理,因而文本文件的名字也能够由容器注入:
class ColonMovieFinder... public ColonMovieFinder(String filename) { this.filename = filename; }
随后,需求通知PicoContainer:各个接口辨别与哪一个完成类联系关系、将哪一个字符串注入MovieFinder组件。
private MutablePicoContainer configureContainer() { MutablePicoContainer pico = new DefaultPicoContainer(); Parameter[] finderParams = {new ConstantParameter("movies1.txt")}; pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams); pico.registerComponentImplementation(MovieLister.class); return pico; }
这段设置装备摆设代码凡是位于另外一个类。关于我们这个例子,运用我的MovieLister 类的冤家需求在本人的设置类中编写适宜的设置装备摆设代码。固然,还可以将这些设置装备摆设信息放在一个独自的设置装备摆设文件中,这也是一种常用的做法。你可以编写一个类来读取设置装备摆设文件,然后对容器实行适宜的设置。虽然PicoContainer自身其实不包括这项功用,但另外一个与它关系严密的项目NanoContainer供给了一些包装,答应开辟者运用XML设置装备摆设文件保管设置装备摆设信息。NanoContainer可以剖析XML文件,并对底下的PicoContainer实行设置装备摆设。这个项目标哲学看法就是:将设置装备摆设文件的格局与底下的设置装备摆设机制别离开。
运用这个容器,你写出的代码大约会是如许:
public void testWithPico() { MutablePicoContainer pico = configureContainer(); MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West", movies[0].getTitle()); }
虽然在这里我运用了结构函数注入,实践上PicoContainer也支撑设值办法注入,不外该项目标开辟者更引荐运用结构函数注入。
Spring 框架是一个用处普遍的企业级Java 开辟框架,此中包含了针对事务、耐久化框架、web使用开辟和JDBC等经常使用功用的笼统。和PicoContainer一样,它也同时支撑结构函数注入和设值办法注入,但该项目标开辟者更引荐运用设值办法注入——恰恰合适这个例子。
为了让MovieLister类承受注入, 我需求为它界说一个设值办法,该办法承受类型为MovieFinder的参数:
class MovieLister... private MovieFinder finder; public void setFinder(MovieFinder finder) { this.finder = finder; }
相似地,在MovieFinder的完成类中,我也界说了一个设值办法,承受类型为String 的参数:
class ColonMovieFinder... public void setFilename(String filename) { this.filename = filename; }
第三步是设定设置装备摆设文件。Spring 支撑多种设置装备摆设方法,你可以经过XML 文件实行设置装备摆设,也能够间接在代码中设置装备摆设。不外,XML 文件是比拟幻想的设置装备摆设方法。
<beans> <bean id="MovieLister" class="spring.MovieLister"> <property name="finder"> <ref local="MovieFinder"/> </property> </bean> <bean id="MovieFinder" class="spring.ColonMovieFinder"> <property name="filename"> <value>movies1.txt</value> </property> </bean> </beans>
因而,测试代码大约就像下面如许:
public void testWithSpring() throws Exception { ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml"); MovieLister lister = (MovieLister) ctx.getBean("MovieLister"); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West", movies[0].getTitle()); }
除后面两种注入技巧,还可以在接口中界说需求注入的信息,并经过接口完成注入。Avalon框架就运用了相似的技巧。在这里,我起首用容易的典范代码阐明它的用法,前面还会有更深化的会商。
起首,我需求界说一个接口,组件的注入将经过这个接口实行。在本例中,这个接口的用处是将一个MovieFinder实例注入承继了该接口的工具。
public interface InjectFinder { void injectFinder(MovieFinder finder); }
这个接口应当由供给MovieFinder接口的人一并供给。任何想要运用MovieFinder实例的类(例如MovieLister类)都必需完成这个接口。
class MovieLister implements InjectFinder... public void injectFinder(MovieFinder finder) { this.finder = finder; }
然后,我运用相似的办法将文件名注入MovieFinder的完成类:
public interface InjectFinderFilename { void injectFilename (String filename); }
class ColonMovieFinder implements MovieFinder, InjectFinderFilename...... public void injectFilename(String filename) { this.filename = filename; }
然后,还需求用一些设置装备摆设代码将一切的组件完成拆卸起来。容易起见,我间接在代码中完成设置装备摆设,并将设置装备摆设好的MovieLister 工具保管在名为lister的字段中:
class Tester... private Container container; private void configureContainer() { container = new Container(); registerComponents(); registerInjectors(); container.start(); }
设置装备摆设的任务分两步,经过查找键值注册组件,同其他示例十分相似。
class Tester... private void registerComponents() { container.registerComponent("MovieLister", MovieLister.class); container.registerComponent("MovieFinder", ColonMovieFinder.class); }
新的一个注册注入器的步调是注册依靠的组件。每个注入接口需求一些用来注入依靠工具的代码。这里我经过运用容器的注册注入器工具来完成。每个注入器工具完成了注入器接口。
class Tester... private void registerInjectors() { container.registerInjector(InjectFinder.class, container.lookup("MovieFinder")); container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector()); }
public interface Injector { public void inject(Object target); }
当依靠是一个为这个轻易编写的类时,为这个组件完成注入器接口自身是成心义的,就像我在movie finder中所做的那样。关于普通的类,例如字符串,我在一段设置装备摆设代码中运用了外部类。
class ColonMovieFinder implements Injector...... public void inject(Object target) { ((InjectFinder) target).injectFinder(this); }
class Tester... public static class FinderFilenameInjector implements Injector { public void inject(Object target) { ((InjectFinderFilename)target).injectFilename("movies1.txt"); } }
让后再测试中运用这个容器。
class IfaceTester... public void testIface() { configureContainer(); MovieLister lister = (MovieLister)container.lookup("MovieLister"); Movie[] movies = lister.moviesDirectedBy("Sergio Leone"); assertEquals("Once Upon a Time in the West", movies[0].getTitle()); }
容器运用了声明的注入器接口来盘算出依靠,而且注入器注入了准确的依靠。(详细的容器完成在这里并非甚么主要的技巧,这里我不做展现。)
依靠注入的最大益处在于:它消弭了MovieLister类对详细MovieFinder完成类的依靠。如许一来,我就能够把MovieLister类交给冤家,让他们依据本人的情况拔出一个适宜的MovieFinder完成便可。不外,Dependency Injection形式并非打破这层依靠关系的独一手腕,另外一种办法是运用Service Locator形式。
Service Locator形式面前的根本思惟是:有一个工具(即办事定位器)晓得怎么取得一个使用顺序所需的一切办事。也就是说,在我们的例子中,办事定位器应当有一个办法,用于取得一个MovieFinder实例。固然,这不外是把费事换了一个模样,我们依然必需在MovieLister中取得办事定位器,终极失掉的依靠关系如图3 所示:
图3:运用Service Locator 形式以后的依靠关系
本文中的一切译文仅用于进修和交换目标,转载请务必注明文章译者、出处、和本文链接。 2KB翻译任务按照 CC 协定,假如我们的任务有进犯到您的权益,请实时联络我们。
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务