资讯专栏INFORMATION COLUMN

深入理解依赖注入

e10101 / 1272人阅读

摘要:上面这部分代码不变,还是通过在构造器中传入依赖的方式初始化依赖调用这里,调用方无需了解内部对的依赖。而配置一般用于上自动扫描并注入的代码如下这里只给出直接在依赖对象上添加注解的形式,还可以通过构造器和注入依赖,这里就不多说了。

前言

相信所有面试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);
    }
}


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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/69658.html

相关文章

  • 深入理解IoC(控制反转)、DI(依赖注入

    摘要:引述最近看设计模式以及代码,对于控制反转以及依赖注入这些概念非常困惑,于是找了一些资料,以下是对于控制反转的一下理解。其中最常见的方式叫做依赖注入,简称,还有一种方式叫依赖查找。在软件工程中,依赖注入是种实现控制反转用于解决依赖性设计模式。 引述 最近看设计模式以及laravel代码,对于控制反转以及依赖注入这些概念非常困惑,于是找了一些资料,以下是对于控制反转的一下理解。 概念 Io...

    xcc3641 评论0 收藏0
  • 深入剖析 Laravel 服务容器

    摘要:划下重点,服务容器是用于管理类的依赖和执行依赖注入的工具。类的实例化及其依赖的注入,完全由服务容器自动的去完成。 本文首发于 深入剖析 Laravel 服务容器,转载请注明出处。喜欢的朋友不要吝啬你们的赞同,谢谢。 之前在 深度挖掘 Laravel 生命周期 一文中,我们有去探究 Laravel 究竟是如何接收 HTTP 请求,又是如何生成响应并最终呈现给用户的工作原理。 本章将带领大...

    abson 评论0 收藏0
  • Laravel深入学习1 - 依赖注入

    摘要:然而,我们需要注意的是仅是软件设计模式依赖注入的一种便利的实现形式。容器本身不是依赖注入的必要条件,在框架他只是让其变得更加简便。首先,让我们探索下为什么依赖注入是有益的。继续深入让我们通过另一个示例来加深对依赖注入的理解。 声明:本文并非博主原创,而是来自对《Laravel 4 From Apprentice to Artisan》阅读的翻译和理解,当然也不是原汁原味的翻译,能保证9...

    sunsmell 评论0 收藏0
  • 深入理解控制反转(IoC)和依赖注入(DI)

    摘要:本文一大半内容都是通过举例来让读者去理解什么是控制反转和依赖注入,通过理解这些概念,来更加深入。这种由外部负责其依赖需求的行为,我们可以称其为控制反转。工厂模式,依赖转移当然,实现控制反转的方法有几种。 容器,字面上理解就是装东西的东西。常见的变量、对象属性等都可以算是容器。一个容器能够装什么,全部取决于你对该容器的定义。当然,有这样一种容器,它存放的不是文本、数值,而是对象、对象的描...

    HollisChuang 评论0 收藏0
  • Laravel深入学习2 - 控制反转容器

    摘要:控制反转容器控制反转使依赖注入变得更加便捷。有瑕疵控制反转容器是实现的控制翻转容器的一种替代方案。容器的独立使用即使没有使用框架,我们仍然可以在项目中使用安装组件来使用的控制反转容器。在没有给定任何信息的情况下,容器是无法实例化相关依赖的。 声明:本文并非博主原创,而是来自对《Laravel 4 From Apprentice to Artisan》阅读的翻译和理解,当然也不是原汁原味...

    worldligang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<