依赖注入和控制反转初见

Inversion of Control Containers and the Dependency Injection pattern扎翻译

原文地址:控制反转和依赖注入
参考1:Object Builder Application Block
参考2:深入理解依赖注入

总结

依赖注入是一个模式,一个模糊的概念,而控制反转是实现依赖注入的一种实现方法。
实现控制反转有三种方式:

  1. 构造器注入
  2. setter注入
  3. 接口注入
    除了控制反转可以实现依赖注入外,还有服务定位器模式也可以解除一定的依赖性。

一个简单的例子

从这里开始翻译:

In this example I'm writing a component that provides a list of movies directed by a particular director. This stunningly useful function is implemented by a single method.

在这个例子中,我正在写一个提供由特定导演指导的电影列表组件,这个非常有用的功能是通过一个方法来实现的。

MovieLister.class

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

这个函数的实现非常的幼稚,它需要一个finder对象(我们很快就知道)返回它的每一场电影,然后只是遍历这个 list (allMovies),筛选出由特定导演指导的电影。我不会去修补这个天真的代码,因为这是本文真正意义上的基础。

这篇文章的真正要点就是这个finder对象,特别的说,我们如何将list对象和特定的finder对象连接起来。有趣的原因是,我希望moviesDirectedBy方法可以和所有电影如何存储的方法完全独立,所以在所有情况下,方法都可以引用finder对象,并且finder对象也会知道如何返回findAll方法,我可以定义一个finder的接口来解决这个问题

public interface MovieFinder {
    List findAll();
}

目前这些都很好的解耦,但是在某些时候,我需要想出一个具体的类来存放这些电影,在这种情况下,我把这些放到了我的lister构造器中。

MovieLister.class

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

总代码如下:

MovieLister.class类

public class MovieLister{

    private MovieFinder finder;
    
    public MovieLister(){
        // ColonDelimitedMovieFinder 冒号分割的文本 获取movie列表
        finder = new ColonDelimitedMovieFinder("movies1.txt");
    }
    
    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()]);
  }
}

MovieFinder.class类

public interfave MovieFinder{
    List findAll();
}

实现类的名称来源于我从冒号分割的文本中获取我的列表,我会告诉你多余细节,毕竟只是一些实现。

现在,如果只是我自己而使用这个类,一切都很好很正常。但是,当我的朋友非常渴望拷贝一份我的代码的时候会发生什么事情呢?如果他们还把电影列表存储在movies1.txt中,那么一切都很美妙。如果他们的存放电影文件的命名不同,那么把文件名称放到一个配置文件中,也很容易。但是,如果他们有一个完全不同的形式存储电影列表:SQL数据库,XML文件,web服务,或者其他格式的文件呢?这种情况下,我们需要一个不同的类来获取这些数据。因为现在我已经定义了一个MovieFinder接口,这不会改变我的moviesDirectedBy方法,但是我仍然需要其他的方法,来正确的实例化finder接口

图1: 在列表类中使用简单创建的依赖关系
图1: 在列表类中使用简单创建的依赖关系

图1: 在列表类中使用简单创建的依赖关系

Figure 1 shows the dependencies for this situation. The MovieLister class is dependent on both the MovieFinder interface and upon the implementation. We would prefer it if it were only dependent on the interface, but then how do we make an instance to work with?
In my book P of EAA, we described this situation as a Plugin. The implementation class for the finder isn't linked into the program at compile time, since I don't know what my friends are going to use. Instead we want my lister to work with any implementation, and for that implementation to be plugged in at some later point, out of my hands. The problem is how can I make that link so that my lister class is ignorant of the implementation class, but can still talk to an instance to do its work.
Expanding this into a real system, we might have dozens of such services and components. In each case we can abstract our use of these components by talking to them through an interface (and using an adapter if the component isn't designed with an interface in mind). But if we wish to deploy this system in different ways, we need to use plugins to handle the interaction with these services so we can use different implementations in different deployments.
So the core problem is how do we assemble these plugins into an application? This is one of the main problems that this new breed of lightweight containers face, and universally they all do it using Inversion of Control.

图一显示了这种状况的依赖关系,MovieLister类同时依赖于MovieFinder接口和实现。我们更喜欢如果MovieLister类只依赖MovieFinder的接口,但是我们该怎么做呢?

在我的一本书中 P of EAA,我们把这种情况称为一个Pluginfinder的实现类在编译时没有链接到程序中,因为我不知道我的朋友们将使用什么。相反,我希望我的lister能够和任何MovieFinder实现类一起工作,并且MovieFinder实现类将会自动的插入到程序中,而不是通过我来掌控。问题是我应该怎样完成这些链接,以便我的lister类对实现类是不知情的,但是它依然可以和finder实例完成工作。

把这些扩展成一个真正的系统,或许我们需要很多这样的服务和组件。在每一种情况下,我们可以通过一个接口(如果组件没有设计接口的话,可以使用组件),来让我们的组件相互关联。但是如果我们希望通过不同的方式部署这个系统,我们需要使用插件来处理这些服务的交互,以便我们可以在不同的部署中使用不同的实现。

所以核心问题是我们如何将这些插件组装到应用文件之中?这也是一些轻量级容器所面临的主要问题之一,通常他们都是通过控制反转来实现的。

控制反转

当这些容器谈到他们如何有用,他们说是因为实现了“控制反转”时候,我感到诧异。控制反转是架构的一个典型特征,所以当这些轻量级框架说因使用了控制反转而特殊,这就像说我的车是特殊的因为它有车轮。

问题是:“他们反转的是哪些方面的控制?”,当我第一次遇到控制反转的时候,是一个主要由用户控制的界面。早期的用户界面是被应用程序所控制,你会有一系列如“输入名称”,”输入地址”的命令;你的程序将驱动提示并承载每一个响应。使用图形(甚至基于屏幕的)UI,UI框架将会包含这个主循环,并且,你的程序将会提供事件处理程序来代替屏幕上的各个区域。这个程序的主要控制权被颠倒了,从你转移到了框架。

因此,我认为我们需要一个更具体的名称来表示这种模式,控制反转(Inversion of Control)是一个过于笼统的术语,因此人们感到困惑。因此与很多IOC提倡者进行了大量讨论之后,我们决定使用Dependency Injection(DI 依赖注入)这个词。

我将开始讨论各种形式的依赖注入,但是我想指出的是,使用插件实现来去除依赖并不是应用程序类的唯一方法。你可以使用的另一种模式是服务定位器模式,在完成解释依赖注入之后,我会讨论这个问题。

依赖注入的形式

依赖注入的基本思想是有一个分隔的对象,一个装配器,这个装配器有适当的finder接口实现来填充lister类中的字段,从而得到图二的依赖关系图

依赖注入的依赖关系
依赖注入的依赖关系

图二:依赖注入的依赖关系

有三种主要方式的依赖注入,我目前叫他们构造函数注入,setter注入和接口注入。如果你在目前关于依赖注入的讨论中阅读过,你会听到这些被称之为type 1 IOC(接口注入),type 2 IOC(setter注入),type 3 IOC(构造器注入)的内容。我发现数字名字很难记住,这就是为啥我在这儿使用了我曾经叫的名字。

带有PicoContainer的构造器注入

我从展示如何使用轻量级框架picocontainer开始.我从这里开始的根本原因是我的几位曾在ThoughtWorks重要的同事,在PicoContainer 的开发中非常的活跃(没错,这是一种企业裙带关系)
PicoContainer使用构造器来决定如何将finder实现类注入到MoveiLister类之中。为了这个工作,MovieLister类需要声明一个构造器,它包含了它需要注入的所有东西。
MovieLister.class

public class MoveiLister{

    private MovieFinder finder;
    
    public MoveiLister(MovieFinder finder){
        this.finder = finder;
    }
    
    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()]);
  }
}

finder 本身也会被picocontainer容器所管理,同理,文本文件的文件名也会被注入到这个容器中

ColonMovieFinder.class

public class ColonMovieFinder{
    private String filename;
    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;
}

这些配置代码通常设置在一个不同的类中。在我们的例子中,每一个使用我的lister类的朋友可能会在某些启动类中编写他们自己的配置代码。当然,在一个单独的配置文件中编写配置信息是很常见的。你可以写一个类去读取配置文件并恰当的设置容器。虽然PicoContainer本身不包含这个功能,但是有一个名为NanoContainer的项目与之紧密相连,它提供了相关的包装,允许你使用xml配置文件。这样的一个纳米级容器,将会解析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同样支持setter注入,尽管它的开发者更喜欢构造器注入。

setter注入和spring

spring框架是企业级Java开发的广泛使用的框架,它包含事务抽象层,持久层框架,web应用程序开发和JDBC,像PicoContainer框架一样,它同样支持构造器注入和setter注入,但是它的开发人员跟倾向于setter注入,这使得它成为这个例子的合适选择。

为了让MovieLister接受注入,我在该类中定义了set方法

MovieLister.class

public class MovieLister{
    private MovieFinder finder;

    public MoveiLister(MovieFinder finder){
        this.finder = finder;
    }
    
    public void setFinder(MovieFinder finder) {
        this.finder = finder;
    }
    
    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()]);
  }
}

同样的,我也为filename定义了一个set方法

public class ColonMovieFinder{
    private String filename;
    
    public ColonMovieFinder(String filename){
        this.filename = filename;
    }
    
    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是在某些情况下使用这种方式的框架的例子,稍后我会进一步讨论,但是现在,我将使用一些简单的示例代码。

服务器定位模式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 我一直在努力摆脱孤独,没想到现在学会了享受孤单。 或许每个人都在别人的欢庆里掩藏着失败的悲伤。 这是一次极其细腻的...
    齐家能阅读 151评论 0 0
  • 升级系统之后发现原来的pod install 等方法不管用了,提示如下错误: -bash: /usr/local/...
    慭慭流觞阅读 3,214评论 0 0
  • 园子里有二三十个品种的进口月季,到了这个季节,陆陆续续绽放,每天有花开,每天都惊喜。
    秋风素影阅读 248评论 1 1