学习应用程序框架永远绕不过的一个话题就是控制反转(IoC)和依赖注入(DI),这两个概念总是令初学者感到困惑,然而这两个概念却是贯穿现代应用程序框架(Application Framework)的最基本的概念,必须要掌握。所以笔者将通过本文带大家了解一下什么是IoC、什么是DI。
IoC,全称Inversion of Control,中文译名控制反转。IoC是一种编程思想,是框架的基本特征,没有IoC特性的我们一般称之为库(Library)。
所谓控制也很好理解,就像游戏的玩家能操控游戏里的角色让他做出玩家想要的动作、公司的老板能控制公司向他想要的领域发展、程序员则能控制说我究竟要用什么指令或某代码库的某某方法去完成数据的计算、转移或存储。能掌控/控制某些事务呢叫拥有自主权,想干什么干什么。在IT的世界里则是开发者调用库时,想用哪个方法用哪个,是相对自由的。
那么反过来,控制反转是什么呢?简单来讲就是被控制。被控制的一方是没有自主权的,就比如公司的业务部门,如果领导不下放办公软件的选择权给员工,那么员工此时就是被控制的一方,领导要让你用Office你就得用Office,领导让你用WPS你就得用WPS,这就和IoC容器很像了,IoC容器就像你的领导,对于某个接口的实现,它给你什么你拿着用就行了,至于它让你用什么,这你不用管。这里提到的IoC容器和IoC是不同的,具体是什么,笔者会在下面讲到。
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容器技术有着诸多的优点,笔者简单列举几点。
应用框架是一个容易令人混乱的词笔者更愿意称之为应用流程框架,应用流程框架指的是通过固定程序流程、部分方法由开发者实现的框架。如果你开发过手机应用,通过onButtonClick、onApplicationExit等事件驱动的手机应用GUI框架就是此类的典型。别的有如服务器应用的Spring’s web MVC、Spring Batch以及Java锁的基本AQS抽象类(别称AQS框架)等都是应用流程框架的应用。框架的特征就是限制了开发者自由发挥的权力,必须在框架的条条框框之下执行,框架之下开发者写的代码只能被框架Call,而不能Call框架,这是框架区别于库的最大特征。
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的理解,希望读者能通过本文了解到这几个概念的含义和区别。