依赖倒置、依赖注入、控制反转经常出现在一些后端的框架和架构设计方面的资料中,它们的概念比较类似,以至于经常傻傻分不清。
依赖倒置
首先我们来讲一下依赖倒置(Dependency Inversion Principle,简称DIP)。DIP是SOLID原则里的依赖倒置原则,在设计模式的书籍中通常都会提到。
依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。 其核心思想是:要面向接口编程(OOD),不要面向实现编程。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户端与实现模块之间的耦合,我们可以总结一下:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类。
以上是DIP的定义,然后关于依赖的传递方式主要有以下三种:
- 构造函数传递依赖对象
- Setter方法传递依赖对象
- 接口声明依赖对象
举一个例子:比如宝马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=[]));`
参考: