控制反转容器&依赖注入模式
Inversion of Control Containers and the Dependency Injection pattern
翻译:坚强2002
源文档 <http://www.martinfowler.com/articles/injection.html>
轻量级容器在Java社区近来可是风起云涌,这些容器能将来自不同项目的逐渐集结为一个内聚的应用程序。这些容器都是基于一个共同的模式,这个模式决定了容器如何完成组件装配,人们统称之为:“控制反转” "Inversion of Control"。本文将深入探讨这个模式的工作机制,并给它一个具体的名字:“依赖注入”Dependency Injection,并与服务定位器(Service Locator,后文将延续使用此译法)模式进行比较。在二者中进行选择不重要,关键是一个二者都遵循的的原则:应该将应用和配置分离.
本文内容
- 组件&服务
- 简单示例
- 控制反转
-
依赖注入的形式
- 使用PicoContainer进行构造器注入
- 使用Spring框架进行属性注入
- 接口注入
-
使用服务定位器
- 为Locator使用分离接口
- 动态服务定位器
- Avalon:Locator Injection双管齐下
-
选择
- 服务定位 VS 依赖注入
- 构造器注入VS属性注入
- 代码配置还是配置文件
- 将配置从应用中分离
- 更多话题
- 结论
在Java世界的企业级应用中有一个有趣的现象:很多人都在尝试做主流J2EE技术的替代品,这多出现在开源社区。这一方面很大程度上反映了主流J2EE技术的沉重和复杂,另一方面这其中诚然也有很多另辟蹊径极富创意。一个常见的问题就是如何将各种元素组织装配起来:Web控制层和数据接口由不同的团队开发而且团队间彼此知之甚少,你如何从中斡旋使其配合工作?很多框架都曾尝试这个问题,一些还在这个方向做了分支,致力于提供通用的各层组件组装解决方案。这些框架通常被称为轻量级容器,例如:PicoContainer Spring .
这些容器的背后有一些有趣的设计原则做支持,这些原则是不拘泥于具体容器和Java平台的。这里我将对这些原则进行探索,例子是java的但我相信同我的大部分文章一样这些原则也适用于其它面向对象环境,特别是.NET.
组件&服务
将元素装配在一起,这个话题一开始就让我陷入棘手的术语问题:"Service""component"对于这两个概念的定义,你轻而易举找到长篇累牍而观点截然相反的文章。所以我先将二者的使用意图进行澄清。
我这里的“component”组件是指一个软件单元,它可以应用程序被使用但是不能被改变,组件的作者对这个应用程序没有控制权。不能修改我是指不能修改组件的源代码,但我们可以通过作者允许的方式对组件行为进行扩展。(译者注:比如NLog组件的扩展LayoutRenderer)
服务Service的概念和组件类似它是由外部应用程序调用。组件和服务主要的区别在于我认为是:组件是可以本地调用的(可以是jar包 程序集 dll 或是源代码引入);服务则是通过远程接口进行同步或者异步调用的(比如web Service ,消息系统,RPC,socket)本文我将使用"服务"一词以蔽之,文中的多数逻辑也适用于本地组件。实际上,你往往徐呀一些本地框架以更好的访问远程服务。但是使用“组件或者服务”这样的说法太啰嗦了,拗口也难写,且“服务"这个词当下更流行些。
简单示例
为了能让问题更具体一些,我通过一个例子来讨论这个话题。像我所有超简单的例子一样,这个例子简单的不真实,但希望它能够让你看清到底发生了什么而不至于纠缠于真实问题的种种细节。
这个例子中我写了一个组件提供某位导演执导的电影列表。实现这个精彩功能只需要一个方法:
<!--<br><br>Code highlighting produced by Actipro CodeHighlighter (freeware)<br>http://www.CodeHighlighter.com/<br><br>-->//classMovieLister
publicMovie[]moviesDirectedBy(Stringarg){
ListallMovies=finder.findAll();
for(Iteratorit=allMovies.iterator();it.hasNext();){
Moviemovie=(Movie)it.next();
if(!movie.getDirector().equals(arg))it.remove();
}
return(Movie[])allMovies.toArray(newMovie[allMovies.size()]);
}
本文真正关心的是finder对象,或者说如何将lister对象和finder对象联系起来。这个问题有趣的原因在于我要期望那个漂亮的movieDirectedBy要独立于影片的存储方式。所以所有方法都会引用一个finder,finder对象可以完成findeAll的功能。我们可以把这个抽取出来做成接口
public interface MovieFinder {
List findAll();
}
现在已经完成了很好的对象解耦,但是我需要用一个实体类来完成电影的查找工作时就要涉及到一个具体类;这里我把代码放在lister的构造器中
//class MovieLister...
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}
这个实体类的名字就能表达这样一个事实:我们需要从一个逗号分隔的文本文件列表中获得影片列表。具体的实现细节我省略掉了,只要知道这是一种实现方式就可以了。
如果这个类只要我自己用一点问题都没有。但是如果我的一位朋友也惊叹于这个精彩的功能并想使用它,那会怎样呢?如果他们也把影片列表存放在文本文件中并使用逗号分隔,而且把文件名改成“movies1.txt”那么一点问题也没有。如果仅仅是电影列表的名字不同那也没有问题,我可以从配置文件中读取。但是如果他们使用完全不同的存储介质呢?比如SQL数据库,xml文件,Web Service,哪怕只是另外一种格式规则存储的文本文件呢?这样我们就需要一个新的类来获取数据。由于已经抽取了一个MovieFinder接口,我可以不修改moviesDirectedBy 方法,我还是希望通过别的途径获得合适的movieFinder实现类的实例。
图1:在MoiveLister类中简单创建MoiveFinder实例时的依赖关系
上图展现了这种情况下的依赖关系:MovieLister类同时依赖于MoiveFinder接口及其实现。我们当然更期望MoiveLister只依赖于接口,但是我们如何得到一个获得一个MoiveFinder的实例呢?
在我的《企业级应用模式》一书中,我们把这种情况称为插件Plugin:MoiveFinder不是在编译时就加入程序的,因为我不知道我的朋友会怎么用什么样的finder。我想让我的MoiveLister类能与任何MoiveFinder实现配合工作,并且允许在运行时加载完全不用我的控制。现在的问题就是如何设计这个连接使MoiveLister类在不知道实现类具体细节的前提下与其协作。
将这种情况推广到真实系统中,我们或许又数十个服务和组件。每种情况我们都可以把使用组件的形式加以抽象,抽取接口并通过接口与组件进行交互(如果组件没有提供一个接口那么可以通过适配暗度陈仓);但是我们希望用不同的方式部署系统,就需要使用插件方式来处理服务间的交互,这样我们才有可能在不同的部署方案中使用不同的实现。
所以现在核心的问题就是我们如何将这些插件集结在一个应用程序中?这恰恰是新生代轻量级容器面对的主要障碍,而它们都无一例外的选择了控制反转。
控制反转
当这些容器的设计者谈话时会说这些容器是如此的有用,因为他们实现了“控制反转”。而我却深感迷惑,控制反转是框架的共有特征,如果说一个框架以实现了控制反转为特点相当于说我的汽车有轮子。
问题是它们反转了什么?我第一次接触控制反转它关注的是对用户界面的控制。早期的用户界面全由程序控制。你会设计一系列的命令:类似于“请输入姓名”“请输入地址”你的程序会显示提示信息并取得用户响应。在图形化(甚至或者是基于触摸屏)用户界面中,用户界面框架会维护一个主循环,你的应用程序只需要为不同的区域设计事件和处理函数就可以了。这里程序的主要控制就发生了反转:从应用程序转移到了框架。
因而对于新生代的容器,它们要反转的就是如何定位插件的具体实现。在我简单的例子中,MovieLister类负责MovieFinder的定位:它直接就实例化了一个子类。这样依赖,MoiveFinder也就不是插件了,因为它不是在运行时加载的。这些容器的方法是:只要插件遵守一定转化规则那么一个独立的程序集模块便可以注入到lister。
结果就是我需要给这个模式一个更具体的名字。控制反转太宽泛了,因而常常让人迷惑。通过和一些IoC爱好者商讨之后我们命名为依赖注入.
我将开始讨论各式各样的依赖注入,但是要先指出的一点是:要消解应用程序对插件的依赖,依赖注入绝不是不二法门。你也可以通过使用Service Locator模式做到这一点,讨论依赖注入之后之后我们会谈到Service Locator服务定位模式。
依赖注入的形式
依赖注入的基本思想是:有一个独立的对象--一个装配器,它获得实现了Finder接口合适的实现类并赋值给MovieLister类的一个字段。现在的依赖情况如下图所示:
图2依赖注入器的依赖关系
依赖注入的形式主要又三种:构造器注入,属性注入,接口注入。如果你关注最近关于依赖注入的讨论资料你就会发现这就是其中提到的类型1IoC 类型2IoC 类型3IoC。数字往往比较难记,所以我这里使用了名称命名。
使用PicoContainer进行构造器注入
首先我通过使用一个轻量级的容器PicoContainer来实现构造器注入。之所以要从这里开始是因为我在ThoughtWorks公司的几个同事在PicoContainer的开发社区很活跃,是的,可是说是一种偏爱。
PicoContainer使用构造器来决定如何把一个finder的实现类注入到lister类中。要达到这个目的MoiveLister类必须要生命一个构造器而且要包含足够的注入信息。
//class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
finder自身也是由PicoContainer管理的所以文本文件的名字也可以通过容器注入来实现。
//class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
下面要做的就是告诉PicoContainer哪些接口和哪些类实现关联,哪些字符串注入到finder组件。
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;
}
这段配置代码通常位于另外一个类。我们这个例子,每一个使用我的lister类只要在自己的配置类编写适当的配置代码就可以了。当然通常的做法是把这些配置信息放在一个单独的配置文件中。你可以写一个类来读取配置文件来设置容器,尽管PicoContainer不包含这个功能在另一个有紧密关系的项目NanoContainer 中提供了一些包装,允许你使用XML配置文件。NanoContainer 解析并对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框架进行属性注入
Spring框架是一个应用广泛的企业级Java开发框架。它包含了对事务、持久化框架、Web应用开发和JDBC的功能抽象。和PicoContainer一样它也支持构造器注入和属性注入,但是它的开发者推荐是使用属性注入--用到这个例子里正合适。
为了让我的Moive Lister能接收注入,我要给它添加一个赋值方法:
//class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
类似的我们为filename也添加这样一个方法
//class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
第三步就是设置配置文件。Spring支持多种配置方式,你可以通过XML文件配置或者通过代码,XML配置文件是比较理想的方式。
<beanid="MovieLister"class="spring.MovieLister">
<propertyname="finder">
<reflocal="MovieFinder"/>
</property>
</bean>
<beanid="MovieFinder"class="spring.ColonMovieFinder">
<propertyname="filename">
<value>movies1.txt</value>
</property>
</bean>
</beans>
测试代码大致是这样的:
2ApplicationContextctx=newFileSystemXmlApplicationContext("spring.xml");
3MovieListerlister=(MovieLister)ctx.getBean("MovieLister");
4Movie[]movies=lister.moviesDirectedBy("SergioLeone");
5assertEquals("OnceUponaTimeintheWest",movies[0].getTitle());
6}
7
接口注入
第三种注入技术是在接口中定义注入需要的信息并通过接口完成注入。 Avalon 就是使用了这种技术的框架的典型例子。稍后我会讨论更多相关内容,现在这个例子先用简单的代码来实现。
使用这个技术我先要定义一个接口,并用这个完接口成注入。这个接口的作用就是把finder实例注入到实现了该接口的对象中。
voidinjectFinder(MovieFinderfinder);
}
这个接口由提供MovieFinder的类提供。任何要使用MovieFinder的实体类都必须实现这个接口,比如lister。
//classMovieListerimplementsInjectFinder
publicvoidinjectFinder(MovieFinderfinder){
this.finder=finder;
}
我使用类似的方法将文件名注入到finder实现中:
publicinterfaceInjectFinderFilename{
voidinjectFilename(Stringfilename);
}
//classColonMovieFinderimplementsMovieFinder,//InjectFinderFilename
publicvoidinjectFilename(Stringfilename){
this.filename=filename;
}
接下来我还需要一些配置代码将这些组件的实现包装起来,简单起见我就在代码里完成了:
//classTester
privateContainercontainer;
privatevoidconfigureContainer(){
container=newContainer();
registerComponents();
registerInjectors();
container.start();
}
配置分成了两个阶段,通过定位关键字来注册组件,这和其它的例子一样:
//classTester
privatevoidregisterComponents(){
container.registerComponent("MovieLister",MovieLister.class);
container.registerComponent("MovieFinder",ColonMovieFinder.class);
}
下一步就是注册要依赖组件的注入器,每一个注入接口都需要一些代码来注入到依赖的对象。这里我使用容器来完成注入器的注册。每一个注入器对象都实现了注入接口。
classTester
privatevoidregisterInjectors(){
container.registerInjector(InjectFinder.class,container.lookup("MovieFinder"));
container.registerInjector(InjectFinderFilename.class,newFinderFilenameInjector());
}
publicinterfaceInjector{
publicvoidinject(Objecttarget);
}
当依赖设计成是一个为容器而写的类那么它对组件自身实现接口注入是有意义的,就像这里我对moivefinder做的修改一样。对于普通类,比如string,我使用内部类来完成配置代码。
//classColonMovieFinderimplementsInjector
publicvoidinject(Objecttarget){
((InjectFinder)target).injectFinder(this);
}
//classTester
publicstaticclassFinderFilenameInjectorimplementsInjector{
publicvoidinject(Objecttarget){
((InjectFinderFilename)target).injectFilename("movies1.txt");
}
}
//Theteststhenusethecontainer.
classIfaceTester
publicvoidtestIface(){
configureContainer();
MovieListerlister=(MovieLister)container.lookup("MovieLister");
Movie[]movies=lister.moviesDirectedBy("SergioLeone");
assertEquals("OnceUponaTimeintheWest",movies[0].getTitle());
}
容器使用声明了注入器的接口来指明注入器和实体类之间的依赖关系。(具体用什么容器实现不重要,我也不会做展示这只会让你笑我)
使用服务定位器
依赖注入的关键优势是它消除了lister类对具体finder类实现的依赖。这样就可以让我的朋友方便的将一个特定的实现插入到他们的应用环境中了。注入绝不是唯一出路,另外一个方案就是使用Service Locator:服务定位器模式。
服务定位器的基本思想是一个对象知道如何获得应用程序所需要的所有服务。在我们的例子里服务定位器就应该有一个方法返回所需的finder实例。这只是减少了一点负担,从下图的依赖关系可以看出我们还是需要在lister中获得定位器。
图3服务定位器的依赖关系
这里我把服务定位器做成一个单件注册表。lister可以在实例化时通过服务定位器获取一个finder的实例。
class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
public static MovieFinder movieFinder() {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
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;
}
public ServiceLocator(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
Here's the test code.
class Tester...
public void testSimple() {
configure();
MovieLister lister = new MovieLister();
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
我常听到这样的抱怨服务定位器不是什么好东西因为你无法替换服务定位器返回的实例也就无法进行测试。当然要是你设计很糟糕遇到了这些麻烦在所难免。在这个例子服务定位器就是一个简单的数据容器,可以简单修改就可以创建适用于测试的服务。
对于更复杂的情况我可以从服务定位器派生子类并将子类传递给注册表类变量。另外我可以让服务定位器暴露出来一个静态方法而不是直接访问该类实例的变量。我还可以使用特定的线程存储提供特定线程的服务定位器。所有这些变化都不会修改使用服务定位器的Client.
一种对服务定位器的改进思路是不设计成单件形式。单件的确是实现注册表的简单方法,但这这只是实现决定很容易改变它。
为定位器使用分离接口
上面的简单方案有一个问题,lister类依赖于整个服务定位器哪怕它仅仅使用一个服务。我们可以通过使用分离的接口来改变这种对服务定位器的依赖,lister使用的是它想要使用的那部分接口。
相应的lister类的提供者也应该提供这样一个定位器接口,使用者可以通过这个接口获得finder。
public interface MovieFinderLocator {
public MovieFinder movieFinder();
The locator then needs to implement this interface to provide access to a finder.
MovieFinderLocator locator = ServiceLocator.locator();
MovieFinder finder = locator.movieFinder();
public static ServiceLocator locator() {
return soleInstance;
}
public MovieFinder movieFinder() {
return movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
注意这里由于使用了接口我们就不能再使用静态方法直接访问服务,我们必须在类中获得定位器实例进而使用各种需要的服务。
动态服务定位器
上面的例子是静态的,服务定位器拥有你想要的各种服务。这还不是唯一的路子,你可以使用动态服务定位器,它允许你在其中注册需要的任何服务并在运行时按需获取服务。
下面的例子中,服务定位器使用map来保存服务信息,而不是放在字段中,并提供了一个通用的方法来获取服务。
class ServiceLocator...
private static ServiceLocator soleInstance;
public static void load(ServiceLocator arg) {
soleInstance = arg;
}
private Map services = new HashMap();
public static Object getService(String key){
return soleInstance.services.get(key);
}
public void loadService (String key, Object service) {
services.put(key, service);
}
配置的功能就是通过特定的关键字来加载服务
class Tester...
private void configure() {
ServiceLocator locator = new ServiceLocator();
locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
ServiceLocator.load(locator);
}
//通过同样的关键字使用服务
//class MovieLister...
MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
总体上讲我不喜欢这个方法,尽管它的确灵活但是它不够直观明了。我只有通过字符串关键字才能定位一个服务,我更倾向比较明了的方法因为通过看接口定义就可以很清楚的知道如何获得一个特定的服务。