控制反转容器与依赖注入模式

    

控制反转容器与依赖注入模式

Martin Fowler

            Java社区中现在正热火朝天的讨论一种可以让不同项目中的组件集成到一个应用中的轻量级容器。这些容器背后隐藏的是一种通用的,关注于如何集成的模式,他们用了一个非常通用的名称来命名:控制反转。在接下来的这篇文章中,我会详细阐述这种模式的工作原理,我为这种模式去了一个更为贴切的名字:依赖注入,然后和服务定位程序进行比较。至于对于他们的选择,我觉得并非那么重要,重要的是到从应用中分离配置的这种意识。

                                                            最后一次更新于:2004.01.23

目录

·        组件与服务

·        一个简单的例子

·        控制反转

·        依赖注入的形式

o   PicoContainer实现构造注入

o   Spring中的设置器注入

o   接口注入

·        使用服务定位程序

o   Using a Segregated interface for the Locator

o   A Dynamic Service Locator

o   Using both a locator and injection with Avalon

·        如何取舍

o   服务定位VS依赖注入

o   构造器注入VS依赖注入

o   代码抑或配置文件

o   从使用中分离配置

·        进一步探讨

·        总结思考

 

企业级Java领域有个有趣的现象,有非常大量活动致力于建立可以替代主流J2EE技术的技术,这些活动大部分属于开源的。这些中有许多是为了对抗复杂度过高的主流J2EE技术,但是大部分是另辟蹊径的探索而且十分有创意。企业级Java领域有个有趣的现象,有非常大量活动致力于建立可以替代主流J2EE技术的技术,这些活动大部分属于开源的。这些中有许多是为了对抗复杂度过高的主流J2EE技术,但是大部分是另辟蹊径的探索而且十分有创意。一个普遍的问题是如何集成不同的元素:如何让这个网站架构应用在另一个数据库上,而这个网站架构和数据库是有两个团队开发,他们彼此对对方知道得很少。许多框架已经针对这个问题提出了自己的方案,其中有的提供了一个通用的机制用以集成不同层间的组件。这些就被称之为轻量级的容易,如PicoContainerSpring.

 这些容器背后是一些设计原则,目前这些容器的平台是java平台。下面我将会阐述这些原则。例子中的语言时java,但就像我所阐述的其他原则一样,他们也适用于其它平台,特别是.net平台。

组件与服务

       集成这个话题让我想起了关于组件,服务等相关的错综复杂的概念。你可以很轻易得找到关于这些概念的冗长,自向矛盾的文章。下面是我对这些概念的定义。

            组件意味着一组将要被使用的软件,这些软件不会被改变,超出了软件作者的控制。不会改变的意思是,使用他们的应用程序不会修改这些组件的源码,尽管他们可以在源码作者的允许下通过修改代码来扩展行为。

            服务类似于被外部程序已用的组件。主要的区别在于这里的组件主要应用于本地(如jar文件,程序集,动态连接库)。服务将会被远程调用通过一些远程接口,无论是同步还是异步(Socket,RPC,web 服务,Soap等)

            这篇文章中用得最多的是服务,但是大多数的情况下可以适用于组件。通常情况下,你会需要许多的本地类库来便捷的访问远程服务。但是用组件和服务来表达这个概念太过于让人麻烦,而且服务目前更流行一些。

一个简单的小例子

            为了使这些更具体,我会用一个小例子来开始。像其他文章中的一样,这将是一个极其简单的例子,简单到可能和实际不大相符,但足以让你体会到我的意图而非陷入到真实例子的沼泽之中。

            这个例子中我会写一个组件用来提供某一个导演的作品列表,这个让我沉醉的功能只包含一个函数。

 

 


 1class MovieLister
 2    public Movie[] moviesDirectedBy(String arg) {
 3        List allMovies = finder.findAll();
 4        for (Iterator it = allMovies.iterator(); it.hasNext();) {
 5            Movie movie = (Movie) it.next();
 6            if (!movie.getDirector().equals(arg)) it.remove();
 7        }

 8        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
 9}

10

 

 

 

            函数的实现真的是很简单,他会像一个寻找器对象(稍后提及)返回它所知道的每一部电影。然后遍历返回器返回的列表。代码虽然简单,但我不打算修改它,因为对于我来说足以。

            这篇文章的中心是寻找器对象,或更进一步的说是我们如何连接这个列表对象和寻找器对象。至于这里为什么这么重要是因为我想要做的是让方法movieDirecteBy完全部依赖于电影列表是如何存储的。既然所有的方法关联到一个寻找器,而且所有的寻找器都知道如何相应findAll方法。所以可以把寻找其定位一个接口。

 


1 public interface MovieFinder {
2     List findAll();
3 }

 

            现在列表与寻找器之间已经很好地实现了解藕,但是必须有一个具体的类来提供影片。这里我把这部分代码放在了列表对象的构造函数里
 

       

1 class MovieLister
2   private MovieFinder finder;
3   public MovieLister() {
4     finder = new ColonDelimitedMovieFinder("movies1.txt");
5   }

       实现类的名称来源于我对文件内容格式的定义-这个文件以冒号分隔不同的影片名称。下面我们来看一下细节方面的问题。(不知道这一句什么意思:afterall thsipoint is just that there’s some implementation

            如果我只是个人用这段代码,那很好。但是当我的朋友认为这个功能棒极了,想把这段代码拷贝过去,那会发生什么呢?如果他们也把他们的电影存放一个名为movies1.txt的文本文件里并且影片名以冒号隔开,那这样很好,代码运行也很正常。如果他们的影片文件的名称是另外一个,那也可以接受,我们只需要把文件名作为一个属性就可以了。但是如果他们存储在一个完全不同的位置呢,例如SQL数据库中,一个XML文件中,或者放在Web Service里面,或者只是另一个各式的文本文件。这种情况下,我们需要以另外一种方式去获取数据。现在因为我定义了一个接口MovieFinder,这样就不用修改我的moviewDirecteBy方法了.但是我仍旧需要一个合适的方法来得到实现了寻找器接口的类。

           

                         图
1 :列表对象直接依赖于寻找器

 

            1列出了这种情况下的依赖关系。列表类即依赖于MovieFinder接口也依赖于接口的实现(注:虽然这种依赖,仅仅是在实例化的时候)。我们比较喜欢的方式是仅仅依赖于那个接口,但是问题是如何创建那个要使用的实例呢?

            在我的著作<<P of EAA>>中,我们把这种情况称之为插件(Plugin),寻找器的实现类在编译时并没有关联到程序 ,因为我并不知道我的朋友要使用它。相反的我希望我的列表关联到任何一种实现,然后对于任何一种实现可以在任何时候插入到程序中。现在的问题是如何在列表类忽略实现类的情况下仍旧可以找到相应的实现类。

            联系到现实情况中,我们有数不清的类似的服务与组件。每一种情况我们都要通过他们的接口获取我们需要的功能(如果组件没有接口的概念我们可以用适配器来代替),但是如果我们想以不同的方式来部署我们的系统,我们就需要利用插件来管理我们与这些组件,服务的交互了。

            所以问题的核心在于我们如何组装这些插件使之成为一个完整的应用?这是轻量级容器所面临的问题,通常情况下他们解决的办法都是控制反转。

控制反转

            当这些容器在吹嘘由于他们实现了控制反转而如何如何厉害,我就觉得非常迷惑。控制反转是框架的一个平常的特性,所以当他们说这些轻量级容器多么特殊而仅仅是由于他们实现了控制反转就好比说还“嗨,看我的车子多特别啊,它有轮子哎!”

            问题在于他们反转的是哪一方面的控制?我第一个遇见控制反转的时候,主要是用户接口的控制。早期的用户接口是被应用程序控制的。你可能 会有这些概念,“输入姓名”,“输入地址”,你的程序手机输入并对每一个做出响应。通过图形界面用户界面库可能会包含这个循环,相反的你的程序会提供屏幕不同字段的事件响应。这样一来,程序的主要控制权被反转了,从你的程序中转移到框架中了。

            而对于这种新的容器的反转在于如何找到一个插件的实现。在上面的小例子里列表类直接实例化相应的寻找器来得到它。这样的话寻找器就不会是一个插件了。这些容器的做法是保证这些插件的使用遵循一些惯例,这些惯例允许单独的模块注入寻找器的实现到列表类中。

            因此我认为我们应该有更确切的对于这种模式,控制反转太泛化了。经过与IOC倡议者的讨论之后我们把这种模式称之为依赖注入。

            下面我将要阐述依赖注入的各种形式,但是要指出的是这并不是唯一的去除应用程序类与插件类之间依赖的饿唯一方式。其他的方式如:服务定位,你也可以尝试。我们讨论完依赖注入再来讨论它。

依赖注入的各种形式

            依赖注入的基本思想是有一个单独的对象_装配者,它会在列表类内部为寻找器的实现设置一个字段,从而使依赖关系如图2所示

 

                                          
2 :依赖注入器的依赖关系

 

            有三种形式的依赖注入。这里我所使用的名称是构造器注入,设置器注入,接口注入。如果你正在参与这方面的讨论你应该会知道他们会被称为类型1IOC(接口注入),类型2IOC(设置器注入),类型3IOC(构造器注入)。我找了许多名字,但他们都非常难记住,这就是为什么我在这里用这些名字。

通过PicoContainer实现构造器注入

            这里我会用PicoContainer来阐述这种注入是如何实现的。我以此作为开始主要是因为我在ThougtWorks的几位同事在开发PicoContainer上非常活跃(是的,就象你想的,有点儿偏袒自个人

            PicoContainer用构造器决定如何注入寻找器的实现到列表类中。首先,电影列表必须声明一个构造器来包括它需要被注入的所有东西。

 


1 class MovieLister
2     public MovieLister(MovieFinder finder) {
3         this.finder = finder;       
4 }

 



     
寻找器同样也接受PicoContainer的管理,这样才可以把文件名注入到寻找器中

       

1 class ColonMovieFinder
2     public ColonMovieFinder(String filename) {
3         this.filename = filename;
4     }


     
PicoContainer然后需要被告知那个实现类将要被关联到那个接口,和需要注入到寻找器的字符串。

       

1     private MutablePicoContainer configureContainer() {
2         MutablePicoContainer pico = new DefaultPicoContainer();
3         Parameter[] finderParams =  {new ConstantParameter("movies1.txt")};
4         pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
5         pico.registerComponentImplementation(MovieLister.class);
6         return pico;
7     }

    典型的做法是把配置代码放在不同的类里。在我们的例子中,每一个用到我的列表类的朋友也许会在他们自己的装配类里写上合适的配置代码。当然把这段配置信息放在一个单独的配置文件里更为常见。你可以写一个配置文件然后设置容器。虽然PicoContainer并不包含这个功能,但是有一个想关联的饿项目NanoContainer为你的XML配置文件提供了合适的包装类。它会分析XML然后配置每一个潜在的PicoContainer.这个项目的指导思想是分离配置文件的格式与项目的实现机制。

    为了使用这个容器你的代码或许会想下面那样。

 


1     public void testWithPico() {
2         MutablePicoContainer pico = configureContainer();
3         MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
4         Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
5         assertEquals("Once Upon a Time in the West", movies[0].getTitle());
6 }

 


虽然我在这个例子中使用了构造器注入,但PicoContainer同样支持设置器注入,尽管它的开发者更喜欢设置器注入。
 
通过Spring实现设置器注入
 
      Java企业级应用开发中Spring是应用广泛的一个框架。它包括事务,持久化框架,web应用开发和JDBC的抽象层。象PicoContainer一样他同样既支持构造器和设置器注入,但是它的开发者倾向于设置器注入-对于这个例子也是一个很好的选择。
 
      为了使影片列表类接受注入须为列表类设置一个设置器方法



         

class MovieLister
    private MovieFinder finder;
  public void setFinder(MovieFinder finder) {
    this.finder = finder;
  }
      同样的为文件名设置设置器



         

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>
 
 

 

            测试代码如下



        

1     public void testWithSpring() throws Exception {
2         ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
3         MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
4         Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
5         assertEquals("Once Upon a Time in the West", movies[0].getTitle());
6     }
接口注入

           

       第三种注入技术用接口来实现。框架Avalon是这方面的佼佼者。稍后我们再讨论它,现在我们只一个简单的例子来论述之.

            首先我们需要定义一个接口,下面就是在寻找器中定义的接口。




        

1 public interface InjectFinder {
2     void injectFinder(MovieFinder finder);
3 }
      这样的接口会被任何提供MovieFinder的接口的组件定义。它需要被使用寻找器的一方来实现,例如下面的列表类。



       

1 class MovieLister implements InjectFinder
2     public void injectFinder(MovieFinder finder) {
3         this.finder = finder;
4     }
5 class ColonMovieFinder implements MovieFinder, InjectFinderFilename
6     public void injectFilename(String filename) {
7         this.filename = filename;
8 }
然后,我们需要一段配置代码来组装这些实现。下面是一段简短的测试代码。



       

 1 class Tester
 2     private Container container;
 3  
 4      private void configureContainer() {
 5        container = new Container();
 6        registerComponents();
 7        registerInjectors();
 8        container.start();
 9 }
10 
这段配置代码分两个阶段,注册组件的情况与前面的例子十分相似。



        

1 class Tester
2   private void registerComponents() {
3     container.registerComponent("MovieLister", MovieLister.class);
4     container.registerComponent("MovieFinder", ColonMovieFinder.class);
5   }
      新加的一个步骤是注册注入器--用来注入依赖的组件。每一个注入接口需要一些代码来注入依赖对象。这里的做法是注册注入器对象。每一个注入器对象实现注入器接口。



       

 1 class Tester
 2   private void registerInjectors() {
 3     container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
 4     container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
 5   }
 6 public interface Injector {
 7   public void inject(Object target);
 8   }
 9  
10 
当被依赖的对象是一个需要被容器写入的类的时候,让组件实现注入器接口是可行的,就像我在这里的做法一样。但是对于平常的类,例如字符串类,我会一个带有配置代码的内部类来实现它。



       

 1 class ColonMovieFinder implements Injector
 2   public void inject(Object target) {
 3     ((InjectFinder) target).injectFinder(this);        
 4   }
 5 class Tester
 6   public static class FinderFilenameInjector implements Injector {
 7     public void inject(Object target) {
 8       ((InjectFinderFilename)target).injectFilename("movies1.txt");      
 9     }
10 }
11 
测试代码中使用这个容器。



       

1 class IfaceTester
2     public void testIface() {
3       configureContainer();
4       MovieLister lister = (MovieLister)container.lookup("MovieLister");
5       Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
6       assertEquals("Once Upon a Time in the West", movies[0].getTitle());
7     }
这里的容器用声明的注入接口来找出依赖点以及注入这些被依赖点的注入器。

进一步探讨

       在这篇文章中,主要论述了利用依赖注入及服务定位程序的基本问题。有许多这方面的主题值得继续关注,但我没有过多的时间去挖掘这些。其中行为生命周期是这些方面中较有意思的一个。一些组件有不同的生命周期时间:实例的停止与开始。另一个问题是这些容器中越来越令人感兴趣的关于以方面编程为导向的部分。虽然目前在这篇文章中并没有涉及到这些内容,但是我确实希望在这篇文章中更多的阐述一下,或者重写写一篇关于他们的文章。

    你可以在这些轻量级容器的网站上找到更多关于这方面的讨论。浏览一下PicoContainer Spring网站,它可以带给你更多这方面的信息和更为高级的话题。

总结

       目前这些轻量级容器都有一个共同的地方就是他们都有一个潜在的相同的地方-如何服务于组件-依赖注入器模式。依赖注入是服务定位程序的一个很好的替代品。创建应用程序类的时候这两者差不了多少,但我认为服务定位程序会更流行一下因为它更为直白一些。尽管如此如果你的类中利用多个应用程序的组件,那么依赖注入更适合你。

       如果你使用依赖注入,这里有好几种方式你可以选择。我建议你用构造器注入,除非你遇到较为棘手的问题,那你可以尝试一下设置器注入。如果你想使用这些容器,那么最好一个即支持构造器注入也支持设置器注入的。

       服务定位程序与依赖注入的选择并非那么重要,重要的是从服务的使用中分离服务配置的意识。

 

注:本文只翻译了依赖注入部分,服务定位部分并未翻译,以后有时间再说吧(初次翻译,敬请斧正:-) )

原文:http://martinfowler.com/articles/injection.html

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页