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

手写不易,转载请注明出处,谢谢

在Java社区中,涌现了许多轻量级的容器,这些容器可帮助将来自不同项目的组件组装成一个 内聚 的应用程序。这些容器的底层构建和连接方式的常见模式,用一个概念来讲,就是我们经常说的“控制反转”。在本文中,我将以更具体的解释名词“依赖注入”深入研究该模式的工作原理,并将其与Service Locator模式(https://www.cnblogs.com/gaochundong/archive/2013/04/12/service_locator_pattern.html)进行对比。从实际使用角度来看,它们之间分离配置的原理比选哪个更为重要。

1.组件和服务

“构建和连接”的主题使我陷入围绕“服务和组件”一词的棘手的术语问题中。您可以轻松找到解释这些术语的相互矛盾的长篇文章。下面这些是我自己对“服务和组件”的一些感悟和用法。

我使用“组件”来描述准备使用的一套软件,该软件可以在不受组件编写者控制的情况下被使用,但不能更改软件源码。当然,在组件编写者允许的情况下可以进行扩展。

服务与组件共同点都是供外部应用程序使用的。主要区别在于,组件在本地使用(例如jar文件,程序集,dll或源导入)。服务将通过同步或异步的某个远程接口(例如,Web服务,消息系统,RPC或套接字)远程调用。

我在本文中主要使用服务,但是许多相同的逻辑也可以应用于本地组件。实际上,通常您需要某种本地组件框架来轻松访问远程服务。但是编写“组件或服务”很容易引起阅读和书写的困扰,并且服务现在更加流行。

2.简单的例子

为了使所有这些更加具体,我将使用一个正在运行的示例来讨论所有这些。像我所有的示例一样,它也是那些超级简单的示例之一。足够小以至于不真实,但希望足以让您直观地了解正在发生的情况 而不是执着于真实示例

在此示例中,我会编写一个组件,该组件提供由特定导演执导的电影列表。该功能是通过单一方法实现的。

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方法完全独立,电影的存储逻辑不能影响它。因此,所有方法最终所做的只是引用一个查找程序,而查找程序所做的就是返回什么结果给该 findAll方法,我可以通过为查找器定义一个接口来实现这一点。

public interface MovieFinder {
    List findAll();
}

现在,所有这些都已经很好地分离(解耦)了,我可以声明一个类来描述movielister,并其代码放入lister类的构造函数中。

class MovieLister...

  private MovieFinder finder;
  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }

实现类具体逻辑来自于以冒号分隔的文本文件中获取的列表。这里不再赘述,毕竟所有这些只是一些实现。

现在,如果我只为自己使用该类,那么一切都很好。但是,当我的朋友对这种出色功能的渴望不知所措,并且想要我的程序的副本时,会发生什么?如果他们还将电影列表存储在以冒号分隔的文本文件“ movies1.txt”中,那么一切都很好。如果他们的电影文件使用不同的名称,则可以很容易地将文件的名称放入属性文件中。但是,如果他们以完全不同的方式存储电影列表的形式:SQL数据库,XML文件,Web服务或只是另一种格式的文本文件怎么办?在这种情况下,我们需要一个不同的类来获取该数据。现在,因为我已经定义了一个MovieFinder接口,所以不会改变我的moviesDirectedBy方法。但是我还需要某种方法来使正确的finder实现实例生效。

图1显示了这种情况的依赖性。该MovieLister取决于 MovieFinder和MovieFinderImpl两者的实施方案。如果它仅依赖于接口,我们会更喜欢它,但是我们如何使一个实例来用(配合)它呢?

在我的P of EAA书中,我们将这种情况描述为插件。查找器的实现类在编译时未链接到程序中,因为我不知道我的朋友将使用什么。相反的,我希望我的列表器(这里是列表器不是查找器哦)可以与任何实现一起使用,并在未来可以不在我控制的情况下完善实现类。问题是如何建立链接,以便我的列表器类不了解实现类,但仍然可以与实例对话以完成其工作。

将其扩展到一个真实的系统中,我们可能会有数十个这样的服务和组件。在大多数情况下,我们都可以抽象出真正使用这些组件的相应接口来进行通信(如果该组件在设计时并未考虑接口,则可以使用适配器)。但是,如果我们希望以不同的方式部署此系统,则需要使用插件来处理与这些服务的交互,以便可以在不同的部署中使用不同的实现。

因此,核心问题是如何将这些插件组装到应用程序中?这是这种新型的轻量级容器面临的主要问题之一,通常它们都使用控制反转(ioc)来解决

3.控制反转

让我感到非常困惑的是,一些容器说自己很有用是因为实现了“控制反转”。控制反转是框架的常见特性,因此说这些轻量级容器之所以特别是因为它们使用控制反转,就好像说我的汽车很特别,因为它带有轮子。

问题是:“控制权在什么方面倒置?” 当我第一次遇到控件反转时,它位于用户界面的主控件中。早期的用户界面由应用程序控制。例如查询界面,您将有一系列命令,例如“输入名称”,“输入地址”;您的程序将驱动提示并获取对每个提示的响应。使用图形(甚至基于屏幕)UI时,UI框架将包含此主循环,而您的程序将为屏幕上的各个字段提供事件处理程序。输入完之后,程序的主控件被反转,从您移至框架。

对于这种新型容器,反转是关于它们如何查找插件实现的。在我幼稚的示例中,列表器通过直接实例化查找器实现。这将阻止查找程序成为插件。这些容器使用的方法是确保插件的任何用户都遵循某种约定,该约定允许单独的汇编器模块将实现注入到列表器中,也就是说需要增加列表器的兼容性。

结论是,我认为我们需要为该模式指定一个更具体的名称。控制反转是一个过于笼统的术语,因此人们会感到困惑。与各种IoC倡导者进行了大量讨论之后,我们决定使用依赖注入这个名称来定义它 。

我将首先讨论各种形式的依赖注入,但是现在我要指出,这并不是解决上述问题(应用程序类与插件实现之间删除依赖)的唯一方法。可以用来执行此操作的另一种模式是Service Locator,在解释完依赖注入之后,我将讨论它。

4.依赖注入的形式

Dependency Injection的基本思想是拥有一个单独的对象,即一个汇编器,该汇编器通过恰当地实现finder接口来迁移到lister类中的依赖属性的,从而产生图2所示的依赖关系图。

依赖注入有三种主要样式。我为它们使用的名称是构造函数注入,Setter注入和接口注入。如果在当前有关控制反转的讨论中读到这些内容,您会听到这些信息分别称为1型IoC(接口注入),2型IoC(Setter注入)和3型IoC(构造函数注入)。我发现很难记住数字名称,这就是为什么我使用这里的名称的原因。

5.PicoContainer的构造方法注入

我将首先展示如何使用名为PicoContainer的轻型容器完成此注入。我从这里开始的主要原因是,ThoughtWorks的几个同事对PicoContainer的开发非常活跃(是的,这是一种公司裙带关系。)

PicoContainer使用构造函数来决定如何将finder实现注入到lister类中。为此,电影列表器类需要声明一个构造函数,其中包括需要注入的所有内容。

class MovieLister...

  public MovieLister(MovieFinder finder) {
      this.finder = finder;       
  }

取景器本身也将由pico容器管理,因此,容器将注入文本文件的文件名。

class ColonMovieFinder...

  public ColonMovieFinder(String filename) {
      this.filename = filename;
  }

然后,需要告知pico容器与每个接口关联的实现类,以及将哪些字符串注入到查找器中。

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

此配置代码通常在其他类中设置。对于我们的示例,使用我的列表器的每个朋友都可以在自己的某些安装程序类中编写相应的配置代码。当然,通常将这种配置信息保存在单独的配置文件中。您可以编写一个类来读取配置文件并适当地设置容器。尽管PicoContainer本身不包含此功能,但是有一个密切相关的项目NanoContainer,它提供适当的包装程序以允许您拥有XML配置文件。这样的纳米容器将解析XML,然后配置底层的pico容器。该项目的理念是将配置文件格式与底层机制分开。

要使用容器,您需要编写类似以下的代码

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也支持setter注入,尽管其开发人员确实更喜欢构造函数注入。

6.Spring的setter注入

Spring框架是企业Java开发一个广泛的框架。它包括事务,持久性框架,Web应用程序开发和JDBC的抽象层。像PicoContainer的它同时支持构造函数和setter注入,但Spring的开发者倾向于更喜欢setter注入-这使得它在这个例子中是一个合适的选择。

为了让我的电影列表管理员接受注入,我为该服务定义了一种设置方法

class MovieLister...

private MovieFinder finder;

public void setFinder(MovieFinder finder) {

this.finder = finder;

}

同样,我为文件名定义了一个setter。

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

7.接口注入

第三种注入技术是定义和使用接口进行注入。Avalon是在某些地方使用此技术的框架的示例。稍后我将详细讨论,但是在这种情况下,我将通过一些简单的示例代码来使用它。

通过这项技术,我首先定义了一个用于执行注入的接口。这是将电影查找器注入对象的界面。

public interface InjectFinder {
    void injectFinder(MovieFinder finder);
}

该界面由提供MovieFinder界面的人定义。任何想要使用查找器的类(例如列表器)都需要实现它。

class MovieLister implements InjectFinder

  public void injectFinder(MovieFinder finder) {
      this.finder = finder;
  }

我使用类似的方法将文件名注入finder实现中

public interface InjectFinderFilename {
    void injectFilename (String filename);
}

class ColonMovieFinder implements MovieFinder, InjectFinderFilename...

  public void injectFilename(String filename) {
      this.filename = filename;
  }

然后,像往常一样,我需要一些配置代码来连接实现。为简单起见,我将在代码中进行。

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

}

当依赖项是为此容器编写的类时,对于组件来说,实现注入器接口本身是有意义的,就像我在电影查找器中所做的那样。对于通用类,例如字符串,我在配置代码中使用内部类。

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 Tester…

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

容器使用声明的注入接口找出依赖关系,并使用注入器注入正确的依赖关系。(我在这里执行的特定容器实现对该技术并不重要,并且我不会展示它,因为您只会笑。)

将list和finder用接口隔离开,finder实现的核心是FinderFilenameInjector,list只能通过实现finder来查找movie,从而真正达到接口级的相互独立

未完待续...

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐