Spring 知识 -- IOC、DIP、DI

本文最后更新于:4 个月前

控制反转 IOC (Inversion of Control )

  1. IoC 是面向对象编程中的一种设计思想,用于降低代码的耦合度,可以说是一种目的,一个目标

  2. IoC 是原则和目标,而具体实现的方式有多种,其中最常见的就是依赖注入 (DI) (dependency Injection),其他的方式还有依赖查找 (DP) ( dependency Lookup)和依赖拖拽等

  3. 实现 IoC 的地方,我们可以称为 IoC 容器

  4. 什么是依赖?

    依赖是面向对象编程中的一个概念,因为是面向对象编程,一个类 A 可能要用到其它的类 B 的属性或者方法,那么就说 A 依赖 B

  5. 什么是控制反转?

    如果 A 依赖 B,那么一般来说,我们要么需要在类 A 中显式使用 new 关键字来创建类 B 的对象

    而使用 IoC,我们只需要声明一下我们需要用到哪些对象,不需要显式的通过 new 来生成依赖对象,反而把对象的创建交给一个 IoC 容器处理和管理

    • 其控制就是指:创建依赖对象

    • 反转就是指: 类 A 需要创建依赖对象 B,但是在类 A 里面不显式创建,而是把创建过程交给第三方,第三方创建好之后,在自动传给我(依赖注入,DI),或者我自己去找(依赖查找,dependency lookup,DL),即反转创建依赖对象这个过程

  6. 为什么要使用 IoC

    1. 最主要原因:面向对象编程时,需要频繁的创建对象,而手动创建管理对象可能会出现问题

      例如:ServiceA 依赖 ServiceB,ServiceB 依赖 ServiceC,而 ServiceC 则同时依赖 ServiceA 和 ServiceB

      1. 考虑循环依赖的问题(Spring 解决循环依赖的方案是三级缓存)

        如果我们考虑可能的循环依赖的问题直接new 对象可能会出问题,因为 ServiceB 依赖的对象 ServiceC 没有创建,这样我们就没法直接在 ServiceA 里面写 new ServiceB(),当然 new 其他 Service 同理,这样程序肯定会报错

        那么如果我们不再类内部创建对象,而是通过构造函数,接口或者 setter 方法传入依赖对象,这样的可以在一定程度上避免在内部创建依赖对象的问题,把这三个对象的创建由其他用于管理的类执行,这样可能可行,但是非常麻烦,因为每一个对象我们都需要来手动管理,然后自己控制赋给相应的对象

        并且,更重要的是,如果你这么做了,实际上就是实现了 IoC,而且是使用 DI 的方式:把依赖的创建权交给了第三方的类,而这个第三方的实现类就是 IoC 容器

        不过,这个是你自己手动实现的,而 Spring 的 IoC 容器已经为我们实现了这个,那么我们直接使用即可

      2. 就算不考虑循环依赖,但是如果创建类之后有一些通用性的配置,每次要自己实现很麻烦

        比如我想要在 Service 里实现事务,那么我就要在里面的每个方法里面自己实现,而业务代码里面充斥着这种模板性的代码就很不好,可读性和维护性都很差

        而如果交给 IoC 容器,你可以通过一些标识(例如注解 @Transactional)告诉容器,容器在创建每一个对象的时候,就可以自己进行相关控制,有的需要加上事务或者是其他的配置,它就可以自动的加上

  7. 总结 IoC 的好处(统一管理,体现了自动装配)

    1. 最重要的就是可以不用程序员手动创建对象,而是把对象的创建和相关管理交给 IoC 容器,IoC 容器自动给你组装好,使程序员可以专注于业务逻辑

      上层无需知道下层的创建过程,将创建细节隐藏,降低了对象之间的耦合度,程序员不需要知道依赖对象如何创建的,将多余的创建对象的责任交给容器,增加了灵活性

    2. 将一些通用性的配置代码交给 IoC 容器统一配置,程序员不需要考虑与业务无关的东西,简化开发,IOC 容器自动给你配置好

    3. IoC 容器容易控制实现单例模式,节省空间,效率高

    4. 方便单元测试 mock

依赖倒置原则 DIP (Dependence Inversion Principle)

  1. 依赖可能产生的问题

    如果类 A 里面有一个属性是类 B,这样 A 直接依赖 B 会因为耦合过高产生一些问题,比如扩展性不好,牵一发而动全身等等

    1. 例子

      例如我之前用 Java 写过的一个 JShell Demo,最开始我在主函数里面对用户输入的指令进行一个个 if-else 判断,再去创建对应的类来调用函数来实现相应的功能

      ...
      if("cd".equals(str)){
          Cd.apply(input);
          // 对于依赖是非静态的方法或者属性
          Cd cd = new Cd(string);
          cd.apply(input);
      }else if("exit".equals(str)){
          Exit.apply(input);
      }else if ...

      这样的代码耦合度就很大,因为功能的实现完全依赖其他的实现类,而主函数就需要对那些依赖类进行创建和管理

      如果要增加新的指令,那么就必须修改主函数增加对命令的 if-else 判断,以此创建新的类,这样非常麻烦,扩展性不好

  2. DIP: 是设计模式的一个原则,体现的是面向接口编程的思想

    1. 例子

      第二次改良我就写了一个接口,所有不同具体的功能类就得去实现这个接口,需要根据字符串生成不同的类不是通过 if-else 判断,而是通过反射实例化对象

      而这样的好处是,我在主函数里面调用接口的方法即可,不需要关心到底是哪一个具体实现类实现了这个接口,因为无论是哪个具体实现类,我们都是要调用那个接口的方法,这样就在主函数里面消除了冗余的 if-else 语句,同时使代码变得更有扩展性,也更加能够维护.

      这样,主函数不依赖具体实现而是依赖接口,而具体实现类又通过自己的具体实现去依赖抽象接口,这样就实现了依赖倒置原则 DIP

下面这段话摘自 Wikipedia: [1]

With the addition of an abstract layer, both high- and lower-level layers reduce the traditional dependencies from top to bottom
Nevertheless, the “inversion” concept does not mean that lower-level layers depend on higher-level layers
Both layers should depend on abstractions that draw the behavior needed by higher-level layers.

通过添加抽象层,高层和较低层都从上到下减少了传统的依赖关系。
但是,“反转”概念并不意味着较低层依赖于较高层。
这两层都应依赖于抽象,这些抽象描述出更高层所需要的行为

  1. IoC 和 DIP 的关系

    IoC 和 DIP 有关,但与之不同,后者涉及通过共享抽象来解耦高层和低层之间的依赖关系。

    IoC 体现的是

    1. 类及其依赖的产生通过容器统一管理并分发,是为了简化开发,实现 IoC 是通过 DI 或者 DL 之类的技术

    2. 而 DIP 是一种经验,体现面向抽象(接口)编程的一种思想

      说白了,我认为,虽然 IoC 只关心类的生命周期,但是实现比较难,而且不用 IoC 框架就得自己实现,来解决类与类之前的关系

      DIP 则不同,不实现 DIP 程序也能使用,但是扩展性和维护性很差,而实现了 DIP,程序可能就很优雅,增强了扩展性和可维护性

依赖注入 DI ( Dependence Injection)

  1. 在 Spring Framework 的 IoC 容器中,配置有三种:xml、annotation 以及 Java config.通过 IoC 容器的控制,把对象的从生成到销毁全部管理起来,再通过依赖注入 DI 的方式将生成的对象提供给所需的类

  2. 可以说依赖是问题,而注入则是解决方案,同理还有依赖查找和依赖拖拽

  3. 依赖注入如何实现

    IoC 容器创建好对象后,

    1. 通过构造函数传入对象

      class A{
          B b;
          A(B b){
              this.b = b;
          }
      }
    2. 通过 setter 方法传入对象

      class A{
          B b;
          public void setB(B b){
              this.b = b;
          }
      }
    3. 通过实现接口

      interface InterfaceName{
          void setB(B b);
      } 
      
      class A implement InterfaceName{
          B b;
          @Override
          public void setB(B b){
              this.b = b;
          }
      }

      前两个可能好理解,但是第三个感觉可能有些鸡肋?

      其实使用接口则是声明了一种依赖的能力,也是可以使用的,只不过 Spring 没有提供

  4. 依赖注入和依赖查找的区别:

    1. 依赖注入只是被动接收,而依赖查找则是需要自己主动去找

总结:

  1. 术语:

    1. 依赖(dependencies):是面向对象编程中的一个概念,因为是面向对象编程,一个类 A 可能要用到其它的类 B,那么就说 A 依赖 B.

    2. 控制反转(Inversion of Control,简称 IoC):为了解偶,将依赖对象的创建交给一个特定的容器(实现了 IoC 的类)来管理对象的生命周期,而不由程序本身实现;是一种设计思想

    3. 依赖倒置原则(Dependencies Inversion Principle,简称 DIP):是设计模式的一个原则,体现的是面向接口编程的思想

    4. 依赖注入(Dependencies Injection,简称 DI)

    5. 上面好像各种实现方式都说有构造函数,setter 方法,接口,这个实际上就是传递依赖的三种方式,而无论是 DI、DIP、IoC,实际上解决的就是依赖的问题


参考文献:

  1. IoC Wikipedia

  2. IoC 知乎

  3. IoC 、DIP 和 DI

  4. DIP Wikipedia


  1. en zh ↩︎