Chinaunix首页 | 论坛 | 博客
  • 博客访问: 274705
  • 博文数量: 80
  • 博客积分: 2269
  • 博客等级: 大尉
  • 技术积分: 836
  • 用 户 组: 普通用户
  • 注册时间: 2009-09-09 11:07
文章分类

全部博文(80)

文章存档

2016年(1)

2013年(8)

2012年(23)

2011年(21)

2010年(21)

2009年(6)

我的朋友

分类: 嵌入式

2012-01-06 14:44:05

编者语:最近研究IoC,在网上搜索到很多网页推荐阅读Martin Fowler的一篇名叫Inversion of Control Containers and the Dependency Injection pattern的文章。点击到该文章页面便吓了一跳:这什么文章啊,简单一个网页PageRank居然是7!要知道,国内几大门户网站也都还没有到这个值呢!也难怪,Martin Fowler被誉为软件开发教父,他的文章,肯定有N多 人拜读。细细读来,感觉大师确实很厉害,文章条理清晰,论证深入,结论明确!本想将此好文翻译推荐给广大网友,可在网上一搜,早有前人将其翻译成中文了。 这里,我将网上流传的中文版整理后,推荐给广大的编程爱好者,希望大师的精彩讲解能够让你对相关问题有一个透彻的理解。同时,我还将其整理成了Word文档,需要的朋友点 这里 下载。

 

摘要:Java
社群近来掀起了一阵轻量级容器的热潮,这些容器能够帮助开发者将来自不同项目的组件组装成为一个内聚的应用程序。在它们的背后有着同一个模式,这个模式决定了这些容器进行组件装配的方式。人们用一个大而化之的名字来称呼这个模式:“控制反转”( Inversion of ControlIoC)。在本文中,我将深入探索这个模式的工作原理,给它一个更能描述其特点的名字——“依赖注入”(Dependency Injection),并将其与“服务定位器”(Service Locator)模式作一个比较。不过,这两者之间的差异并不太重要,更重要的是:应该将组件的配置与使用分离开——两个模式的目标都是这个。
 

目录

组件和服务
一个简单的例子
控制反转
依赖注入的几种形式
    使用PicoContainer 进行构造函数注入
    使用Spring 进行设值方法注入
    接口注入
使用Service Locator
    为定位器提供分离的接口
    动态服务定位器
    Avalon 兼顾服务定位器和依赖注入
作出一个选择
       Service Locator vs. Dependency Injection
    构造函数注入 vs. 设值方法注入
    代码配置 vs. 配置文件
    分离配置与使用
更多的问题
结论和思考
致谢

在企业级Java的世界里存在一个有趣的现象:有很多人投入很多精力来研究主流J2EE 技术的替代品——自然,这大多发生在open source社群。在很大程度上,这可以看作是开发者对主流J2EE技术的笨重和复杂作出的回应,但其中的确有很多极富创意的想法,的确提供了一些可供选择的方案。J2EE开发者常遇到的一个问题就是如何组装不同的程序元素:如果web控制器体系结构和数据库接口是由不同的团队所开发的,彼此几乎一无所知,你应该如何让它们配合工作?很多框架尝试过解决这个问题,有几个框架索性朝这个方向发展,提供了更通用的“组装各层组件”的方案。这样的框架通常被称为“轻量级容器”,PicoContainerSpring都在此列中。

在这些容器背后,一些有趣的设计原则发挥着作用。这些原则已经超越了特定容器的范畴,甚至已经超越了Java平台的范畴。在本文中,我就要初步揭示这些原则。我使用的范例是Java代码,但正如我的大多数文章一样,这些原则也同样适用于别的OO环境,特别是.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 实例时的依赖关系 

1展现了这种情况下的依赖关系:MovieLister类既依赖于MovieFinder接口,也依赖于具体的实现类。我们当然希望MovieLister类只依赖于接口,但我们要如何获得一个MovieFinder子类的实例呢? 

在《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 进行构造函数注入

首先,我要向读者展示如何用一个名为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进行设值方法注入

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.txtvalue>
        
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 InjectFilename
{
    
void injectFilename (String filename);
}

class ColonMovieFinder implements MovieFinder, InjectFilename...
    
public void injectFilename(String filename)
    {
        
this.filename = filename;
    }

现在,还需要用一些配置代码将所有的组件实现装配起来。简单起见,我直接在代码中完成配置,并将配置好的MovieLister 对象保存在名为lister的字段中:

class IfaceTester...
    
private MovieLister lister;
    
private void configureLister()
    {
        ColonMovieFinder finder 
= new ColonMovieFinder();
        finder.injectFilename(
"movies1.txt");
        lister 
= new MovieLister();
        lister.injectFinder(finder);
    }

测试代码则可以直接使用这个字段:

class IfaceTester...
    
public void testIface()
    {
        configureLister();
        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 模式之后的依赖关系 

在这里,我把ServiceLocator类实现为一个Singleton的注册表,于是MovieLister就可以在实例化时通过ServiceLocator获得一个MovieFinder实例。

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;
    }

下面是测试代码:

class Tester...
    
public void testSimple()
    {
        configure();
        MovieLister lister 
= new MovieLister();
        Movie[] movies 
  • 注:暂存的内容只能恢复到当前文章的编辑器中,如需恢复到其他文章中,请编辑该文章并从暂存箱中恢复;或者直接复制以上内容,手工恢复到相关文章。
  • 恢复到编辑器   关闭
  • 阅读(1469) | 评论(0) | 转发(1) |
    给主人留下些什么吧!~~