Xudong's Blog

控制反转与依赖注入

Word count: 1.7kReading time: 6 min
2018/11/14

什么是控制反转

控制反转(Inversion of Control, 缩写IoC),是一种面向对象的设计原则。从字面上理解,就是让控制权从类本身移交给其他类。那么是哪些控制权被反转了?答案是:“依赖对象的创建”这个控制权被移交了。移交给谁了?答案是:IoC容器。

这样做带来的好处就是降低代码之间的耦合度,业务代码的对于底层类的依赖被降低后,底层实现就可以更容易地改变,业务代码也更容易被复用。

很多人都说控制反转可以用好莱坞规则来描述 “Don’t call us, we will call you.”。我们自己不需要自己创建依赖对象,IoC容器会帮我创建。

什么是依赖注入

依赖注入(Dependency Injection,缩写DI),是指动态地向某个类中注入(或者说创建)这个类所需要的依赖。很明显,这个概念和IoC密不可分,有人说DI是IoC的一种实现方式,也有人说DI就是IoC。过分纠结这些是没有必要的,如果你能有自己的理解,那么就会明白这两者究竟是什么关系。其实我们需要一个例子来加深理解。

Robot的依赖

1
2
3
4
5
6
7
8
9
10
public class Robot{  // 我们有一个机器人类,它需要许多组件
private RobotArm arm; // 可以是齿轮驱动的机械臂,也可以是液压驱动
private RobotFoot foot; // 动力装置可以是履带式、轮式
public void move(Position p){
foot.moveTo(p.x, p.y);
}
public void work(Object o){
arm.workWith(o);
}
}

Robot依赖于两个接口RobotArmRobotFoot的实现类。现在的Robot显然没有办法工作,因为依赖的对象没有被创建。于是,我们可以将2、3行的代码替换为

1
2
private RobotArm arm = new GearArm();
private RobotFoot foot = new WheelFoot();

这样,在Robot的对象被创建时,它需要的依赖也会自动的被创建好。这种“依赖注入”,是Java在初始化对象时会帮我们做的。还有另一种方式是在构造函数中对armfoot进行初始化,我们经常做这样的事情。达到的效果和上面是一样的。

Robot的耦合

上面这样做的优点就是简洁明了。缺点就是将RobotGearArm WheelFoot紧密关联在了一起,如果我们想给Robot换一个Arm,就需要再写一个RobotXXXArm的类,或者使用反射技术。所以耦合带来的影响就是不灵活,难以复用,难以改变底层实现。

使用依赖注入

看呐,要解决这些问题,就要使用依赖注入了。我们想使用什么样的Arm,IoC容器就给我们的Robot注入什么样的Arm对象。我们有四种方式可以让IoC容器给我们的Robot注入我们需要的依赖对象(XXArm和XXFoot)。

接口注入

被注入对象如果想要IoC容器为其注入依赖对象,就要实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。

1
2
3
4
5
6
7
public class Robot implements RobotArmCallable{
@Override
public void injectRobotArm(RobotArm arm){
... ...
}
... ...
}

从注入方式的使用上来说,接口注入是现在不甚提倡的一种方式,基本处于“退役状态”。因为它强制被注入对象实现不必要的借口,带有侵入性。而构造方法注入和setter方法注入则不需要如此。

构造方法注入

在被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让IoC容器知道它需要哪些依赖对象。这就叫构造方法注入。

1
2
3
4
5
6
public class Robot{
public Robot(RobotArm arm, RobotFoot foot){
... ...
}
... ...
}

在学习面向对象编程之初,我们就已经会使用这种方法来注入依赖了,构造方法的用途便是这样。只不过现在我们可以更进一步,向构造方法传入参数的工作也可以交给IoC容器。

这种注入方式的优点就是,对象在构造完成之后,就会进入就绪状态,可以马上使用。缺点就是,当依赖对象比较多的时候,构造方法的参数列表会比较长。而通过反射技术构造对象的时候,对相同类型的参数的处理会比较困难,维护和使用上也比较麻烦。而且在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动可能造成维护上的不便。

setter方法注入

对于JavaBean对象,我们通常会通过setXXX()和getXXX()方法来访问对应其private XXX属性。这些setXXX()方法统称为setter方法,getXXX()当然就称为getter方法。

1
2
3
4
5
6
7
8
9
public class Robot{
... ...
public void setArm(RobotArm arm){
... ...
}
public void setFoot(RobotFoot foot){
... ...
}
}

因为方法可以命名,所以setter方法注入在描述性上要比构造方法注入好一些。另外,setter方法可以被继承,允许设置默认值。缺点当然就是对象无法在构造完成后无法立即使用,无法立即进入就绪状态。

基于注解注入

1
2
3
4
5
6
7
import org.springframework.beans.factory.annotation.Autowired;
public class Robot{
@Autowired
private RobotArm arm;
@Autowired
private RobotFoot foot;
}

基于Java5.0之后引入的注解(Annotation),在私有变量前加@Autowired注解(Spring Framework为我们提供的),不需要显式地加入额外代码,便可以让IoC容器注入对应的依赖对象。该方案相当于定义了setter方法,但是因为没有真正的setter方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为用作注入的setter只想让IoC容器访问来注入而并不希望其他人来修改此对象的私有域)。现在Spring也最推荐我们使用注解代替XML配置,所以基于注解注入的方式被使用的越来越多(因为确实很好用)。

尾巴

Robot类被这么一折腾之后,就使得类之间的耦合松散了,这里不仅是RobotArmRobotFoot接口起到了解耦的作用,因为之前高耦合的代码一样使用了接口啊(其实接口并没有发挥效果,它们被错误地使用了),DI很好的诠释了接口的作用。使用了IoC之后,我们得到的额外好处就是便于我们写单元测试,测试驱动开发是一种很受重视的开发方式,没有IoC带来的松耦合,连测试都会变得困难。

用书上的一句话来概括 “IoC是一种可以帮助我们解耦各业务对象间依赖关系的对象绑定方式。”

CATALOG
  1. 1. 什么是控制反转
  2. 2. 什么是依赖注入
    1. 2.1. Robot的依赖
    2. 2.2. Robot的耦合
  3. 3. 使用依赖注入
    1. 3.1. 接口注入
    2. 3.2. 构造方法注入
    3. 3.3. setter方法注入
    4. 3.4. 基于注解注入
  4. 4. 尾巴