[Java] 什么是IoC?什么是DI?它们的区别是什么?

  • Post author:
  • Post category:java




前言


学习应用程序框架永远绕不过的一个话题就是控制反转(IoC)和依赖注入(DI),这两个概念总是令初学者感到困惑,然而这两个概念却是贯穿现代应用程序框架(Application Framework)的最基本的概念,必须要掌握。所以笔者将通过本文带大家了解一下什么是IoC、什么是DI。



IoC


IoC,全称Inversion of Control,中文译名

控制反转

。IoC是一种编程思想,是框架的基本特征,没有IoC特性的我们一般称之为库(Library)。

所谓控制也很好理解,就像游戏的玩家能操控游戏里的角色让他做出玩家想要的动作、公司的老板能控制公司向他想要的领域发展、程序员则能控制说我究竟要用什么指令或某代码库的某某方法去完成数据的计算、转移或存储。能掌控/控制某些事务呢叫拥有自主权,想干什么干什么。在IT的世界里则是开发者调用库时,想用哪个方法用哪个,是相对自由的。


那么反过来,控制反转是什么呢?简单来讲就是被控制

。被控制的一方是没有自主权的,就比如公司的业务部门,如果领导不下放办公软件的选择权给员工,那么员工此时就是被控制的一方,领导要让你用Office你就得用Office,领导让你用WPS你就得用WPS,

这就和IoC容器很像了,IoC容器就像你的领导,对于某个接口的实现,它给你什么你拿着用就行了,至于它让你用什么,这你不用管

。这里提到的

IoC容器和IoC是不同的

,具体是什么,笔者会在下面讲到。



IoC的两种应用


IoC是一种基于“被控制”的思想,根据开发者“被控制/限制”的权力的不同,笔者总结了两种IoC思想的应用:


  • IoC容器

    :组件依赖的某接口具体实现类(Implementation)的选择、实例化以及销毁的权力被IoC容器拿走。开发者不需要关心一个组件到底是如何被构建起来的,而仅需要使用。

  • 应用流程框架

    :软件执行的流程(控制流)的大方向被框架所控制,在需要时开发者编写的模块会被框架调用。



IoC容器

IoC容器有很多,主流的不用说Spring框架里的Core模块里的ApplicationContext,还包括已经停止维护的Seasar框架的S2Container等等,IoC容器就像个装配厂,能帮你按一定规则组装好一个“可立即使用”的组件,而不用繁琐的一大堆new操作,并且对象从创建到回收的生命周期通常也由IoC容器管理,这种即拿即用以及大部分的通用可重用组件的默认组装方式,有诸多好处(下面列举)使得IoC容器成为现代应用软件开发不可或缺的一部分。

笔者简单举个例子,假如我们需要卡车这个“组件”以便能够进行物流运输作业,我们需要轮胎、引擎、中控等等组件去组装一辆卡车。此时我们各类组件分别有两个厂商的产品可选(

现实生活中往往一个产品赛道里品牌数量多达数十种,而这些同类产品往往会遵守同一标准,如某某国标GB或某某国际标准ISO等

)。

interface Wheel { /* 轮胎 */ }
interface Engine { /* 引擎 */ }
interface ControlPanel { /* 中控 */}

final class WheelImplByMichelin implements Wheel { /* 米其林轮胎 */}
final class WheelImplByGoodyear implements Wheel { /* 固特异轮胎 */}

final class EngineImplByHonda implements Engine { /* 本田发动机 */ }
final class EngineImplByBMW implements Engine { /* 宝马发动机 */ }

final class ControlPanelImplByCaska implements ControlPanel { /* 卡仕卡中控 */ }
final class ControlPanelImplByFlyAudio implements ControlPanel { /* 飞歌发动机 */ }

我们有卡车Vehicle类,分别需要轮胎Wheel、引擎Engine和中控ControlPanel。

public class Vehicle {
    /** 轮胎 */
    Wheel wheel;

    /** 引擎 */
    Engine engine;

    /** 中控 */
    ControlPanel controlPanel;

    /* 略过其他需要的组件,复杂的组件需要依赖更多的组件... */

    public Vehicle() {}
}

作为卡车司机,我们可能有三类人,那我们看一下具体都是什么样的。

public abstract class VehicleDriver {
    Vehicle vehicle;
}

一:DIY爱好者,自己组装,

类比传统的软件开发者,此时所有实现类的选择权力都在开发者手上,你可以选择任意自己喜欢的接口实现类

。这很自由,但是自由有自由的代价就是其质量难以保证。究竟我们自己DIY出的东西质量如何这只有天知道了。

DIY一个东西通常需要对其依赖的部分下层组件都有充分的了解,否则容易出现问题,软件开发也一样,就算是高级开发者也很难了解一个项目的全貌掌控所有依赖的组件,以及具体依赖特定实现类会使得项目耦合程度极高,替换特定实现类的成本会非常高

(代码变更、单测、结合测、各类测试、打包、部署等),这种DIY式的开发逐渐在

商业公司

业务开发部门


中消失。

public final class DIYVehicleDriver extends VehicleDriver {
    DIYVehicleDriver() {
        /* 传统使用就需要大量new,此时开发者权力最大,拥有一切组件的选择权 */
        vehicle = new Vehicle();
        vehicle.wheel = new WheelImplByGoodyear();
        vehicle.engine = new EngineImplByHonda();
        vehicle.controlPanel = new ControlPanelImplByCaska();
    }
}

二:品牌车使用者,不自己组装,直接找品牌车厂家要成品,这时厂家就可以直接绑定销售自己同品牌或盟友的其他产品了。类比软件开发中

库的使用者

,库的使用者不关心组件内部到底是如何构建的,

而库的开发者通常为了减少库的外部依赖内部常常会有自己的实现

,各种库的内部实现相信读者如果有阅读过源码一定不会陌生了,就算是JDK同为JUC包的多个锁实现类中、其内部都有多个AQS具体实现类。

/** 品牌车卡车司机 */
public final class BrandVehicleDriver extends VehicleDriver {
    BrandVehicleDriver() {
        /* 库的使用者,通常对库内部依赖不了解,下放组件选择权给库的开发者。*/
        vehicle = BrandVehicle.newBrandVehicle();     
    }
}

/** 品牌车 */
public class BrandVehicle extends Vehicle {

    /** 工厂方法:《推荐配置》《组装品牌车》 */
    public static Vehicle newBrandVehicle() {
        final Vehicle vehicle = new BrandVehicle();
        vehicle.wheel = new WheelImplByBrand(); // 依赖品牌自己的轮胎
        vehicle.engine = new EngineImplByBrand(); // 依赖品牌自己的引擎
        vehicle.controlPanel = new ControlPanelByBrand(); // 依赖品牌自己的中控
        return vehicle;
    }

    /* 某品牌自主实现 */
    private static final class WheelImplByBrand implements Wheel {}
    private static final class EngineImplByBrand implements Engine {}
    private static final class ControlPanelByBrand implements ControlPanel {}
}

三:苦逼打工人,被领导,没有自己选择的权力,给你什么车就开什么车。这类比的就是我们的IoC容器与开发者的关系了,IoC容器就是领导、开发者就是苦逼打工人,开发是不被允许自主选择实现类的,只有领导可以。

public final class NormalVehicleDriver extends VehicleDriver {
    final Vehicle vehicle;

    NormalVehicleDriver(Vehicle vehicle) {
        /* 苦逼打工人无法自主选择使用哪种车,只能被动接受。 */
        this.vehicle = vehicle;
    }
}

不过就算这样,

我们依然会困惑IoC容器的开发者是如何得知这些组件是如何组装的呢?他们开了天眼吗?

其实很简单,领导之上还有大领导(配置)。IoC容器作为中层领导拥有按照约定(convention)组装组件的权力,但如果大领导(配置configuration)发话,IoC容器就得按照大领导的要求去组装。大领导通常不会对通用的常规功能性组件做指示,而部分特殊的业务组件就需要明确做出指示了。



约定大于配置

既然上面提到了约定与配置,就不得不提到

“约定大于配置(Convention Over Configuration)”

这句话了,IoC容器就类似空降的国际专家来当中层领导,IoC容器知道各类标准化的东西如通用组件的组装方式、通用命名方式(有通用命名可以方便查找)等等,也就是说IoC容器知道一套默认的、约定俗成的规则,利用这套规则它可以很轻易地组装市面上大部分通用组件,甚至说如果你公司特有的一些组件也遵循容器的约定(如命名规则)等,容器也是能找到组件并组装的(Auto component scanning),对于违反约定(公司特有)的,则需要通过配置来解决,即需要大领导“指示”。



IoC容器的优点

看了上面的例子,不难看出IoC容器技术有着诸多的优点,笔者简单列举几点。


  1. IoC容器技术能加快开发速度

    。减少大量new、组装对象实例的代码,使得开发者们可以更专注于业务代码,提升了生产力。

  2. IoC容器技术能提高代码的质量

    。大量跨应用的通用组件的构建及使用变得简单(约定大于配置),通过约定的方式降低了引入通用组件可能带来的bug的风险,这些约定的组件构建方式是经过业界千锤百炼的“优良”代码。

  3. IoC容器技术能减少代码的耦合度,增加代码的可测试性

    。IoC容器是经典的“乐高积木”模式,组件与其依赖的组件之间的装配均通过接口匹配的方式实现,只要接口类型对上了,就能拼接这些组件。这样高度模块化的组件在测试时,

    无论是自上而下还是自下而上的测试,都不依赖具体的实现类而变得容易测试。大型项目中高度且完善的自动化测试永远是提高代码质量的最好保障


  4. IoC容器技术能

    大幅降低更换接口实现类的成本


    ,与第3点很像,模块化以及通过接口匹配的装配方式,能很方便的更换相同接口下的不同实现,并利用其高度可测试性带来的自动化测试能显著降低更换成本。

  5. IoC容器技术与AOP很搭

    。面向接口的模块化编程高度切合AOP的思想,与业务无关的类似日志、缓存等功能很方便通过AOP加入,进一步减少业务代码中的与业务无关的代码。



应用流程框架


应用框架是一个容易令人混乱的词笔者更愿意称之为应用流程框架

,应用流程框架指的是通过固定程序流程、部分方法由开发者实现的框架。如果你开发过手机应用,通过onButtonClick、onApplicationExit等事件驱动的手机应用GUI框架就是此类的典型。别的有如服务器应用的Spring’s web MVC、Spring Batch以及Java锁的基本AQS抽象类(别称AQS框架)等都是应用流程框架的应用。

框架的特征就是限制了开发者自由发挥的权力,必须在框架的条条框框之下执行,框架之下开发者写的代码只能被框架Call,而不能Call框架,这是框架区别于库的最大特征



DI


DI,全称Dependency Injection,中文译名

依赖注入

。简单来说,


DI与IoC容器是一个东西


,要注意区分IoC和IoC容器并不是同一个东西,IoC容器的主要推广者之一的

Martin Fowler

就在他的博文《

Inversion of Control Containers and the Dependency Injection pattern

》一文中就提到的因为IoC过于普通容易招致误解,所以他与多个IoC推广者讨论后决定了一个新名字,

依赖注入(DI)

As a result I think we need a more specific name for this pattern.

Inversion of Control is too generic a term, and thus people find it confusing

. As a result with a lot of discussion with various IoC advocates

we settled on the name Dependency Injection

.


1

依赖注入就比IoC容器更加具象化,不那么抽象了。

依赖是什么?依赖就是你办公需要办公桌椅、电脑、办公软件等等没有你就无法继续的一些资源。

依赖注入是什么?依赖注入就是领导让你用某个工位、某台电脑、某个特定办公软件等。



结语


笔者带新人时通常就是使用这一套说辞来解释这几个基本概念。对于这几个概念网上解释的人有很多也很杂,因此本篇其实也是相对主观的一篇文章,对于懂英文的读者笔者十分推荐去阅读Martin Fowler的那篇原文。

笔者在本文使用相对白话一点的语言去讲述自己对IoC、IoC容器与DI的理解,希望读者能通过本文了解到这几个概念的含义和区别。



参考



  1. Inversion of Control Containers and the Dependency Injection pattern – Martin Fowler


    ↩︎



版权声明:本文为ToraNe原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。