控制反转、依赖注入和依赖倒置

Published: Creative Commons Licence

一、前言

控制反转、依赖注入和依赖倒置这几个概念容易搞不太清楚,这里专门写一篇文章梳理一下,一是为了知识上的解惑,二是为了在以后的开发中能实践这些理念和原则,写出更优雅的代码。

二、控制反转

Java 开发应该有过样的体会:写一段简单的程序时,从 main 函数开始往后一步步代码是怎么执行的,都在自己的掌握之中;但是有一天用上了框架,比如说 Servlet,事情就发生了变化,我们只需要实现框架定义好的接口,并在指定的方法内部写逻辑,连 main 方法都不需要写,启动应用,然后自己的方法就神奇地被执行到了。

这其实就是控制反转,是框架经常使用到的一种模式,其核心是反转控制流,由框架来调用应用,而不是应用来调动框架。也被更加形象地称为 Hollywood Principle - "Don't call us, we'll call you"。像 Spring,JUnit,GUI 框架,模板方法设计模式都有用到控制反转的思想。控制反转使得框架拥有了程序的控制权,从而能够定义出一套执行的总体流程,并在合适的位置开放出接口留给用户去扩展。

控制反转是框架的一个基本特性,用 Martin Fowler 的话来说,一个框架声称自己的特性是实现了IOC,就和说一辆车的特性是有四个轮子一样奇怪。如果没有用到控制反转,只是提供了一些列函数供用户端调用,这样的东西我们一般称之为库,比如 guava,apache-commons, http-client 这样的东西。

三、依赖注入

学过面向对象设计的应该都知道要面向接口编程,而不是应该面向实现编程,但是实际开发中,这一点却并不容易做到,如果稍不注意就会和具体的实现绑死。

比如说我们定义了一个抽象接口 Service:

public interface Service {
    void doSomething();
}

然后我们的应用使用该服务:

public class Client {
    private Service service;
    
    public void useService() {
        service.doSomething();
    }
}

看起来很美好,我们的 Client 声明的依赖是 Service 接口,而不是具体的实例。但是现实是,我们无可避免地必须在某一处真正实例化那个 service 变量(不然会得到一个NPE),如果把这个逻辑做在了 Client 类里,那么就会出现如下这么一段碍眼的代码:service = new ServiceImpl1(); 。这下好了,Client 和具体实现类 ServiceImpl1 耦合了起来,如果想要使用新的实现类 ServiceImpl2,就必须要修改 Client 代码,我们之前面向接口编程的美好设想落空了。

依赖注入就是为了解决这个问题而生的,它的核心思路是:客户端只需要声明自己依赖的接口,而不要真正去实例化它,真正的实例会在合适的时机注入到客户端中去。

依赖注入真正实现了面向接口编程的愿景,可以很方便地替换同一接口的不同实现,而不会影响到依赖这个接口的客户端。而且使用了依赖注入后,每个组件只需要关注自己的逻辑,而不需要关心组件之间的组合逻辑,大大降低了系统复杂度。

四、依赖倒置

依赖倒置原则,是面向对象设计原则 SOLID 中的 D(Dependency Inversion),核心要义是:不要让高层依赖底层,而要让底层依赖高层的抽象。听起来和依赖注入很像对不对?实际上,依赖注入的最主要目的,就是为了实现依赖倒置。一个是手段,一个是目的。

阿莱克西斯有一篇讲系统分层的文章 用谁都能看懂的方式来解释系统设计中的分层 非常好地解释了依赖倒置原则,浅显易懂,可以直接去看,我就不复读了。

依赖倒置的意义,可以参考他的另一篇知乎回答:

不使用依赖反转的系统构架,控制流和依赖关系流的依赖箭头是一个方向的,由高层指向底层,也就是高层依赖底层,最明显的特点就是高层包需要import很多底层包里的类,这样的话任何的底层小改动,都可能产生影响高层业务逻辑类的改动的蝴蝶效应;这样的系统耦合严重,维护,模拟,测试,实验和拓展都很困难,可能动不动就要重构和re-architecture;让程序员苦不堪言。而使用依赖倒置,使得底层包依赖高层包。高层包里不会import任何一个底层类。只要interface设计的好,底层的任何变动,都不会影响高层一行code。

另外顺便说一下 SOLID 原则的其他几个:

  • S : 单一职责原则,一个类应该仅具有一种职责。
  • O : 开闭原则,软件应该对扩展开放,对修改关闭。
  • L : 里氏替换原则,程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。
  • I : 接口隔离原则,多个特定客户端接口好于一个宽泛用途的接口。
  • D : 依赖倒置原则,上面说过了。

五、总结

  • 控制反转就是反转控制流,是框架常用的一种模式,由框架调用应用,而非应用调用框架。
  • 依赖注入是一种解决依赖具体实现的方案,客户端只需要声明自己依赖的接口,而不要真正去实例化它,真正的实例会在合适的时机注入到客户端中去。
  • 依赖倒置是一种通用的软件设计原则,不要让高层依赖底层,而要让底层依赖高层的抽象。

六、参考资料

  • https://martinfowler.com/bliki/InversionOfControl.html
  • https://martinfowler.com/articles/injection.html
  • https://www.zhihu.com/question/265433666/answer/337599960
  • https://zhuanlan.zhihu.com/p/32844598