深入理解依赖注入

前言

相信所有面试java开发的童鞋一定都被问到过是否使用过Spring,是否了解其IOC容器,为什么不直接使用工厂模式,以及究竟IOC和DI区别在于哪里这种问题。今天就结合JAVA语言,解释一下究竟是如何衍生出DI模式,以及其在Spring中的实现。

很久很久以前

初学Java,我们一定会学到面向对象的编程思想,以及使用new关键字新建一个对象。假设现在有一个邮件发送系统,该系统包含拼写检查功能。那么本着面向对象的思想以及关注点分离的思想,我们会将其分解为两个类:EmailerSpellChecker。其中,Emailer依赖着SpellChecker提供的服务,这两个类的实现如下:

public class SpellChecker{
    ...
    public void check(){
       ...
    }
}

public class Emailer{
    private SpellChecker spellChecker;
    public Emailer(){
        spellChecker = new SpellChecker();
    }
}

可以看到我们在构造器中使用new新建了一个SpellChecker的对象。

现在我们来分析一下这个实现的不足之处:

  • 可测试性:假设现在我希望测试Emailer的功能是否完善,但是此时SpellChecker并没有完成开发与测试,那么我们将无法对Emailer进行测试。就算SpellChecker已经开发完成,但是我们也无法排除当前的错误是否和SpellChecker的实现无关。
  • 可维护性:假设现在支持多语种,那么我需要分别实现一个EnglishEmailer和FrenchEmailer类。他们的构造函数中分别初始化EnglishSpellChecker和FrenchSpellChecker。以后每增加一个语种都需要新建一个新的Emailer类。而这些类的代码本质上都是重复的。更不要提假设里面

因此我们就需要一种新的初始化依赖的方式。

自己初始化不行,那你给我一个现成的吧!

既然在调用依赖的类中初始化依赖这么麻烦,不如将构建完成的依赖传入调用的类。

public class SpellChecker{
    ...
    public void check(){
       ...
    }
}

public class Emailer{
    private SpellChecker spellChecker;
    public Emailer(SpellChecker spellChecker){
        this.spellChecker = spellChecker;
    }
}
//使用
Emailer email = new Emailer(new EnglishSpellChecker())

从测试性的角度来说,这个代码明显更加易于测试了,我们可以提供SpellChecker的一个Mock实现,如Emailer e = new Emailer(new MockSpellChecker())来对Emailer进行测试。那么这种实现的缺点在哪里呢?

首先,调用Emailer的代码需要知道如何去初始化SpellChecker,而这明显暴露了Emailer的内部实现,违背了信息隐藏的思想。其次,一旦依赖发生变化,比如Emailer还需要依赖一个定时装置Scheduler来实现定时发送邮件,那么所有的调用Emailer的代码都需要发生改变。显然,这种写法的可维护性依然不高。

工厂模式闪亮登场,所有的初始化都交给我了!

那么,我们是否可以将所有对象构建的代码提取出来,像工厂标准件一样生产出来。所有对对象的调用都通过工厂提供。

public class SpellChecker{
    ...
    public void check(){
       ...
    }
}

public class Emailer{
    private SpellChecker spellChecker;
    public Emailer(SpellChecker spellChecker){
        this.spellChecker = spellChecker;
    }
}
//上面这部分代码不变,还是通过在构造器中传入依赖的方式初始化依赖

public class EmailerFactory {
    public Emailer newFrenchEmailer(){
        return new Emailer(new FrenchSpellChecker());
    }
}

//调用
Emailer email = new EmailerFactory().newFrenchEmailer();

这里,调用方无需了解内部对SpellChecker的依赖。无论之后Emailer的依赖发生什么样的变化,客户端代码都不会受到影响。那么这种设计有没有缺陷呢?

当然是有的。Emailer的测试和之前一样,我们可以通过传入Mock的对象来对其进行测试。那么调用Emailer的服务怎么办呀?在调用方看来我们只是依赖着Factory对象,因此我们需要通过定义Factory返回一个Mock对象才行,同时这个对象还不能影响真正的Factory的实现。

除此以外,每当我们对一个新的语种添加支持时,我们都必须添加一段新的代码,如下:

public class EmailerFactory {
    public Emailer newJapaneseEmailer() {
        Emailer service = new Emailer();
        service.setSpellChecker(new JapaneseSpellChecker());
        return service;
    }
    public Emailer newFrenchEmailer() {
        Emailer service = new Emailer();
        service.setSpellChecker(new FrenchSpellChecker());
        return service;
    } 
}

而这两段初始化代码基本上是完全相同的!而假设以后我们需要实现一个全球通用版本。。。
光是无聊的工厂模式代码就要花费我们大量的时间!

我说出你的名字,你敢应吗!

有没有这样一个东西,客户端代码报出它的编号key,它就会返回那个对象的实例。当然这个实例是根据配置生成的。比如Emailer English这样的key,就会返回英语的Emailer。这种思路衍生出了服务定位模式。这个模式相当于站在了所有工厂模式的最前端。它就像是一个老式的电话中转服务,调用服务的人输入服务的唯一编号,即电话号码,而服务定位器找到该服务并返回该服务的实例。调用如下:

Emailer emailer = (Emailer) new ServiceLocator().get("Emailer");

JNDI(Java Naming and Directory Interface)就是该思想下的一个实现。服务的提供方在JNDI上注册服务,之后调用方在JNDI上检索服务,实现二者之间的解耦。

这个模式的问题和工厂模式类似,难以测试以及需要管理共享状态。其次,通过使用String类型的Key来获取服务无法在编译时对服务调用是否正确以及服务类型是否正确进行检查。

这里将不会给出JNDI的具体实现,对JNDI的概念有困惑的可以查看这篇文章

Injector隆重登场

看来,任何和构造对象相关的代码夹杂在业务代码中都会带来麻烦,那么我们可以将这部分代码全权委托给构造框架,业务代码通过依赖注入从而关注于业务本身,而框架可以通过配置甚至是自动的生成对象注入到客户端。从而实现二者的完全解耦。

至此,对象关联图的构造,联系和组装将和业务代码完全无关,这种情况也被成为控制反转(IOC)

不同的框架对于依赖注入的实现是不同的,但是本质上来说,他们都确保了客户端无需在业务代码中了解注入的依赖是如何初始化的。

IOC vs DI

那么IOC和DI之间的区别究竟是什么呢?
IOC这个概念所表示的领域其实超出了依赖注入的范围,它更多强调的是控制反转,也就是说,这个对象是别人替你创建好的。因此DI是IOC的一种实现机制。而控制反转可以运用于更多的场景,如:

  • J2EE应用服务器中的一个模块,比如Servlet
  • 框架自动调用的测试方法
  • 点击鼠标后调用的事件处理器

IOC不仅负责创建对象,还需要管理对象的生命周期。不同的生命周期需要触发不同的调用,这些调用被称为回调函数。除此以外,IOC容器管理的对象需要被打上标记,比如使用@Autowire,@Component注解的类和对象,以及继承了Servlet接口的Servlet才会被Servlet容器管理。

因此我们常见的Spring更像是将IOC和DI思想结合在一起生成的产物。

更多关于IOC VS DI可以参考这篇文章

Spring

Spring是一个轻量级的依赖注入框架,它已经成了所有JAVA开发者无法躲开的开发大礼包。Spring提供了三种依赖注入的方式:XML,注解和Java Config

XML方式曾经非常流行,但是这种方式也逐渐暴露出问题,主要的问题在于无法对注入的依赖进行类型检查,从而导致代码无法在编译期间识别出问题,只能在运行期间抛出异常。现在主要推荐自动扫描并注入以及通过JavaConfig代码来配置。而XML配置一般用于Legacy System上

Autowired

自动扫描并注入的代码如下:

public class Emailer{
    @Autowired
    private SpellChecker spellChecker;

    public class Emailer(SpellChecker spellChecker){
    }    
}

@Component
public class SpellChecker{
    ...
}

@ComponentScan
public class EmailerConfig{
}

这里只给出直接在依赖对象上添加注解的形式,还可以通过构造器和setter注入依赖,这里就不多说了。

Java Config

Java Config则是将配置代码单独提取出来:

@Configuration
public class EmailerConfig{
    @Bean
    public Emailer EnglishEmailer(){
        return new Emailer(new EnglishSpellChecker());
    }
}

当然,这里也可以通过依赖注入的方式来确保传入的对象是单例的(默认情况下Spring生成的对象为单例)

@Configuration
public class EmailerConfig{
    @Bean
    public Emailer EnglishEmailer(SpellChecker spellChecker){
        return new Emailer(spellChecker);
    }
}

clipboard.png
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值