第2章 IoC的基本概念

让别人为你服务

IoC的全称是Inversion of Control,中文通常翻译为“控制反转”,它还有一个别名叫依赖注入(Dependency Injection)。

假定有一提供外汇(Foreign Exchange)新闻的项目,该项目功能是爬取指定网站新闻并存储到本地,最终在FX系统前台展示,这些功能由FXNewsProvider类来完成。

来看下代码:

 
    public class FXNewsProvider {
        // 抓取新闻类
        private IFXNewsListener newsListener;
        // 存储新闻类
        private IFXNewsPersister newPersistener;
 
        // 构造器
        public FXNewsProvider() {
            newsListener = new DowJonesNewsListener();
            newPersistener = new DowJonesNewsPersister();
        }
 
        // 抓取新闻和存储的方法
        public void getAndPersistNews() {
            // 获取所有可用新闻id数组
            String[] newsIds = newsListener.getAvailableNewsIds();
            if (ArrayUtils.isEmpty(newsIds)) {
                return;
            }
            // 循环每条新闻存储进数据库并发布
            for (String newsId : newsIds) {
                FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
                newPersistener.persistNews(newsBean);
                newsListener.postProcessIfNecessary(newsId);
            }
        }
    }
 

我在关键行加了一些注释,可以看到首先我们定义了两个成员变量,一个用于抓取新闻内容,一个用于存储新闻。并且在构造器中,我们提供了DowJones道琼斯新闻社的实现类(DowJonesNewsListener和DowJonesNewsPersister)。这两个类是FXNewsProvider这个新闻提供类所要依赖的类,因此可以被称为“依赖类 ”、“ 依赖对象 ”。

如果我们依赖于某个类,我们最常用的方法就是new它,直接在构造器中新建相应的依赖类,也就是说我们 需要主动去获取依赖的对象

可是回头想想,我们自己每次用到什么依赖对象都要主动地去获取,这是否真的必要?我们最终所要做的,其实就是直接调用依赖对象所提供的某项服务而已。只要用到这个依赖对象的时候,它能够准备就绪,我们完全可以不管这个对象是自己找来的还是别人送过来的。对于FXNewsProvider来说,那就是在getAndPersistNews()方法调用newsListener的相应方法时,newsListener能够准备就绪就可以了。 如果有人能够在我们需要时将某个依赖对象送过来,为什么还要大费周折地自己去折腾?

实际上,IoC就是为了帮助我们避免之前的“大费周折”,而提供了更加轻松简洁的方式。它的反转,就反转在让你从原来的事必躬亲,转为现在的享受服务。所以,简单点儿说,IoC的理念就是,让别人为你服务 !在图2-1中,也就是让IoC Service Provider来为你服务!

image-20220401232113130

上面的FXNewsProvider就是 被注入对象,它被注入了DowJonesNewsListener抓取类和DowJonesNewsPersister存储类,而这两个类也是被依赖对象,FXNewsProvider依赖它们俩。现在,被注入对象需要什么都由IoC Service Provider来提供,与之前直接寻求依赖相比,依赖对象的取得方式发生了反转。

image-20220402192904561

三种依赖注入方式

由三种依赖注入的方式,即构造方法注入(constructor injection)、setter方法注入(setter injection)以及接口注入(interface injection)

举例子:

当你来到酒吧,想要喝杯啤酒的时候,通常会直接招呼服务生,让他为你送来一杯清凉解渴的啤酒。同样地,作为被注入对象,要想让IoC Service Provider为其提供服务,并将所需要的被依赖对象送过来,也需要通过某种方式通知对方

如果你是酒吧的常客,或许你刚坐好,服务生已经将你最常喝的啤酒放到了你面前;

如果你是初次或偶尔光顾,也许你坐下之后还要招呼服务生过来;

还有一种可能,你根本就不知道哪个牌子是哪个牌子,这时,你只能打手势或干脆画出商标图来告诉服务生你到底想要什么!

构造方法注入

构造方法注入,就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表, 让外部(通常是IoC容器)知道它需要哪些依赖对象。

    public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister) {
    	this.newsListener = newsListner;
    	this.newPersistener = newsPersister; 
    }

IoC Service Provider 会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。 构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。这就好比你刚进酒吧的门,服务生已经将你喜欢的啤酒摆上了桌面一样

setter方法注入

当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。以FXNewsProvider为例:

 
    public class FXNewsProvider {
        private IFXNewsListener newsListener;
        private IFXNewsPersister newPersistener;
 
        public void setNewsListener(IFXNewsListener newsListener) {
            this.newsListener = newsListener;
        }
 
        public void setNewPersistener(IFXNewsPersister newPersistener) {
            this.newPersistener = newPersistener;
        }
      	...
    }
 
 

这样,外界就可以通过调用setNewsListener和setNewPersistener方法为FXNewsProvider对 象注入所依赖的对象了。

setter方法注入虽不像构造方法注入那样让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入。这就好比你可以到酒吧坐下后再决定要点什么啤酒,随意性比较强。如果你不急着喝,这种方式当然是最适合你的。

接口注入

被注入对象如果想要IoC Service Provider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。 IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。

实现的接口和接口中声明的方法名称都不重要。重要的是接口中声明方法的参数类型,必须是“被注入对象”所依赖对象的类型。

它强制被注入对象实现不必要的接口,带有侵入性已经不推荐使用了

IoC的附加值

从主动获取依赖关系的方式转向IoC方式,不只是一个方向上的改变,简单的转变背后实际上蕴藏着更多的玄机。要说IoC模式能带给我们什么好处,可能各种资料或书籍中已经罗列很多了。比如不会对业务对象构成很强的侵入性,使用IoC后,对象具有更好的可测试性、可重用性和可扩展性 ,等等。

对于前面例子中的FXNewsProvider来说,当系统中需要追加逻辑以处理另一家新闻社的新闻来源时,问题就来了。

假定新闻社叫MarketWin24。这个时候,你该如何处理呢?首先,毫无疑问地,应该先根据MarketWin24的服务接口提供一个MarketWin24NewsListener实现,用来接收新闻;其次,因为都是相同的数据访问逻辑,所以原来的DowJonesNewsPersister可以重用,我们先放在一边不管。最后,就主要是业务处理对象FXNewsProvider了。因为我们之前没有用IoC,所以,现在的对象跟DowJonesNewsListener是绑定的,我们无法重用这个类了,不是吗?为了解决问题,我们可能要重新实现一个继承自FXNewsProvider的MarketWin24NewsProvider,或者干脆重新写一个类似的功能。

而使用 IoC 后,面对同样的需求,我们却完全可以不做任何改动,就直接使用 FXNewsProvider。 因为不管是DowJones还是MarketWin24,对于我们的系统来说,处理逻辑实际上应该是一样的:根据各个公司的连接接口取得新闻,然后将取得的新闻存入数据库。因此,我们只要根据MarketWin24的新闻服务接口,为MarketWin24的FXNewsProvider提供相应的MarketWin24NewsListener注入就可以了,见代码:

    FXNewsProvider dowJonesNewsProvider = new FXNewsProvider(new DowJonesNewsListener(),new DowJonesNewsPersister());
    ...
    FXNewsPrivider marketWin24NewsProvider = new FXNewsProvider(new MarketWin24NewsListener(),new DowJonesNewsPersister());
    ...

使用IoC之后,FXNewsProvider可以重用,而不必因为添加新闻来源去重新实现新的FXNewsProvider。实际上,只需要给出特定的IFXNewsListener实现即可。

随着开源项目的成功,TDD(Test Driven Developement ,测试驱动开发)已经成为越来越受重视的一种开发方式。对于软件开发来说,设计可测试性良好的业务对象是至关重要的。而IoC模式可以让我们更容易达到这个目的。比如,使用IoC模式后,为了测试FXNewsProvider,我们可以根据测试的需求,提供一个MockNewsListener给FXNewsProvider。在以前不用IoC的时候,我们无法将对DowJonesNewsListener的依赖排除在外,从而导致难以开展单元测试。而现在,单元测试则可以毫无牵绊地进行,下面代码演示了测试取得新闻失败的情形。

定义一个获取新闻失败的listener:

 
 
    public class MockNewsListener implements IFXNewsListener {
        public String[] getAvailableNewsIds() {
            throw new FXNewsRetrieveFailureException();
        }
 
        public FXNewsBean getNewsByPK(String newsId) {
    // TODO
            return null;
        }
 
        public void postProcessIfNecessary(String newsId) { // TODO
        }
    }
 

相应的FXNewsProvider的单元测试类:

 
 
    public class FXNewsProviderTest {
        private FXNewsProvider newsProvider;
 
        @Override
        protected void setUp() throws Exception {
            super.setUp();
            newsProvider = new FXNewsProvider(new MockNewsListener(), new MockNewsPersister());
        }
 
        @Override
        protected void tearDown() throws Exception {
            super.tearDown();
            newsProvider = null;
        }
 
        public void testGetAndPersistNewsWithoutResourceAvailable() {
            try {
                newsProvider.getAndPersistNews();
                fail("Since MockNewsListener has no news support, ➥ we should fail to get above.");
            } catch (
                    FXNewsRetrieveFailureException e) {
                //......
            }
        }
    }
 

疑问:为什么说以前无法排除DowJonesNewsListener依赖?

解答:如果不用IoC,就是主动寻求依赖的方式,代码是这样的:

    public FXNewsProvider() {
      newsListener = new DowJonesNewsListener();
      newPersistener = new DowJonesNewsPersister();
    }

可以看到DowJonesNewsListener被我们硬编码写入的,并赋值给了成员变量newsListener,而在使用了IoC容器的情况下,可以通过构造器将MockNewsListener注入进去,关键代码是newsProvider = new FXNewsProvider(new MockNewsListener(), new MockNewsPersister());

可见, IoC是一种可以帮助我们解耦各业务对象间依赖关系的对象绑定方式。