首页
一文读懂依赖倒置、依赖注入、控制反转

依赖倒置、依赖注入、控制反转经常出现在一些后端的框架和架构设计方面的资料中,它们的概念比较类似,以至于经常傻傻分不清。

依赖倒置

首先我们来讲一下依赖倒置(Dependency Inversion Principle,简称DIP)。DIP是SOLID原则里的依赖倒置原则,在设计模式的书籍中通常都会提到。

依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。 其核心思想是:要面向接口编程(OOD),不要面向实现编程。

依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户端与实现模块之间的耦合,我们可以总结一下:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。

以上是DIP的定义,然后关于依赖的传递方式主要有以下三种:

  1. 构造函数传递依赖对象
  2. Setter方法传递依赖对象
  3. 接口声明依赖对象

举一个例子:比如宝马BMW类、奔驰Benz类都有一个run方法,然后Driver类通过依赖倒置的方式去实现开不同的车。

php示例

//通过接口声明依赖对象
interface IDriver {
    public function drive(ICar $car);
}

//通过构造函数传递依赖对象
class Driver implements IDriver {
    private $car;
    
    public function __construct(ICar $car) {
        $this->car = $car;
    }

    public function drive() {
        $this->car->run();
    }
}

//通过Setter方法传递依赖对象
class Driver implements IDriver {
    private $car;

    public function __construct() {}

    public function setCar(ICar $car) {
        $this->car = $car;
    }

    public function drive() {
        $this->car->run();
    }
}

它带来的好处如下:

  • 可以降低类间的耦合性。
  • 可以提高系统的稳定性。
  • 可以减少并行开发引起的风险。
  • 可以提高代码的可读性和可维护性。

TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的一个高级应用。

控制反转

控制反转(Inversion of Control,简称IoC) 经常被提及到的都是 Java 的 Spring 框架实现,但它并不是一个具体的技术。它跟DIP原则一样也是OOP中的一种设计原则。

它要实现的是如下的过程:

Class A 中用到了 Class B 的对象 b,一般情况下,需要在 A 的代码中显式地用 new 创建 B 的对象。采用依赖注入技术之后,A 的代码只需要定义一个 private 的 B 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 B 对象在外部 new 出来并注入到 A 类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如XML)来指定。

简单来讲就是通过主动的去new一个对象变成了由专门的容器去创建并注入进来。那么为什么叫控制反转呢,我们得清楚两件事情:

  • 谁控制谁,控制什么

    软件系统在没有引入 IoC 容器之前,对象 A 依赖于对象 B,那么对象 A 在初始化或者运行到某一点的时候,自己必须主动去创建对象 B 或者使用已经创建的对象 B。无论是创建还是使用对象 B,控制权都在自己手上。

  • 什么被反转了

    Martin Fowler 在Inversion of Control Containers and the Dependency Injection pattern这篇文章中进行了详细地分析。在使用了 Ioc 之后,获得依赖对象的过程被反转了。控制被反转之后,获得依赖对象的过程由自身管理变为了由 IoC 容器主动注入。这也就引出了依赖注入(Dependency Injection,简写DI)这个概念。

依赖注入

IoC的实现一般分为两种方式:依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup)。 依赖查找已经很少被提及,现在的Ioc实现几乎都是用依赖注入的方式。

所谓依赖注入,就是由IoC容器在运行期间,动态地将某种依赖关系注入到对象之中。

在技术实现上一般都是利用语言的反射机制(Reflection)。通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。很多人都会通过研究源码的方式来理解,比如看 Java 的 Spring 和 php 的 Laravel 实现。但是阅读别人的代码有时候还没有自己去实现一个简单的 Ioc 更能够去理解它。

之前在项目中为了设计一个事件系统,就利用反射实现了一个简单的 Ioc,不过没有通过 xml 去配置,而是一个数组。部分代码如下:

<?php

/**
 * 事件监听总线,调用示例:
 * @example
 *          $this->hooks->_call_hook('event_notifier_bus' , array('TaskMove' , $params=[]));
 *          参数格式:array($param1,$param2)
 *                  $param1:event目录下具体的Event类
 *                  $param2:该event需要传递的参数,格式为数组
 * Class Event_notifier_bus
 */
class Event_notifier_bus {
    //event和对应的listener类配置
    private $notifierMiddleware = array(
        //卡片对应事件
        'TaskCopy' => [
            'Event' => '\Events\Task\Copy' ,
            'Listeners' => [
                '\Listeners\Listener1' ,
                '\Listeners\Listener2'
            ]
        ]
    );

    public function handle($params) {
        //如果参数格式不正确,直接结束
        if ( ! is_array($params)) {
            return;
        }
        //notifier类必须传入
        if (empty($params[0])) {
            return;
        }

        //传入的对应动作类,没有命名空间
        $class = $params[0];
        if ( ! isset($this->notifierMiddleware[ $class ])) {
            return;
        }

        $eventClass = $this->notifierMiddleware[ $class ]['Event'];
        $listenersClass = $this->notifierMiddleware[ $class ]['Listeners'];

        //使用autoloader自动注入
        $filepath = APPPATH . 'events/Autoloader.php';

        if ( ! class_exists($eventClass)) {
            require_once($filepath);
        }
        //event对应的listener类
        $params = $params[1] ?? [];
        (new $eventClass($listenersClass))->notify($params);
    }
}

通过在notifierMiddleware这个数组中配置好对应的事件类和订阅者类,当需要发送事件的时候,使用对应的 key 就可以了。

$this->hooks->_call_hook('event_notifier_bus' , array('TaskCopy' ,$params=[]));`

参考:

  1. Introspection and Reflection in PHP