深刻理解依赖注入

前言

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

好久好久之前

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

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

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

能够看到咱们在构造器中使用new新建了一个SpellChecker的对象。java

如今咱们来分析一下这个实现的不足之处:面试

  • 可测试性:假设如今我但愿测试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
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注个人微信公众号!将会不按期的发放福利哦~

相关文章
相关标签/搜索