“以用户注册,注册完成后发送通知” 为示例,使用伪代码详细讲解 IOC & DI 的进化史,不去深究理论的东西,俗称讲人话。
- Step 1. 用户注册后,发送一封通知邮件
Class Email {
func send() {};
}
Class User {
func register() {
//todo: 注册逻辑
Email email = new Email();
email.send()
}
}
User user = new User();
user.register();
考察如下场景,如果现在需要将发送 email 变为发送 sms,我们就不得不修改代码(高耦合)
Class Email {
func send() {};
}
Class SMS {
func send() {};
}
Class User {
func register() {
//todo: 注册逻辑
SMS sms = new SMS(); // 修改为 SMS
sms.send()
}
}
User user = new User();
user.register();
- Step 2. 使用接口增加可扩展性、使用构造方法传参方式提高可维护性(降低耦合)
interface Sender {
func send();
}
Class Email implements Sender {
func send() {};
}
Class SMS implements Sender {
func send() {};
}
Class User {
Sender sender;
// 含参构造方法,将依赖的对象传递进去
func User(Sender s) {
sender = s;
}
func register() {
//todo: 注册逻辑
sender.send();
}
}
// 使用 email 通知
Sender email = new Email();
User user = new User(email)
// 或者使用 sms 通知
// Sender sms = new SMS();
// User user = new User(sms)
user.register();
到这一步,User 类已经有很好的可扩展性并且通过构造方法传递参数的形式降低了耦合,如果我们还需要修改通知方式,比如使用电话语音通知。我们不必再深入 User 类中修改逻辑,只需要再新建一个类让其实现 Sender 接口即可,在实例化 User 时将新的实现类对象传递进去,即可使用新的通知方式。
Class Voice implements Sender {
func send() {};
}
Sender voice = new Voice();
User user = new User(voice);
user.register();
到这就可以来解释 IOC 了,控制反转(IOC)这个概念我们拆成 “控制” 和 “反转” 来解析
- 反转
在 Step 1 中,我们先 new User,再 new Email,实例化顺序记做 User -> Email
在 Step 2 中,我们使用构造方法传参的形式,将实例化的过程变为,先 new Email,再 new User,实例化顺序记做 Email -> User
当 A 依赖于 B,B 又依赖于 C,我们将实例化顺序,由 A -> B -> C,变为 C -> B -> A,这就是 IOC 中的“反转”
正如 Step 2 那样,我们实现了反转,但是我们会发现,如果依赖关系太深的话,让调用者手动去 new 一堆的依赖,也是非常不便利的,如下
...
C c = new C();
B b = new B(c);
A a = new A(b);
a.doSomething();
由此我们可以来解释 IOC 中的 “控制”
- 控制
使用“某种手段”,来帮助调用者解决手动 new 一堆依赖的问题 ---- 这种手段会自动解析类的依赖链,按照顺序,自动完成依赖链的实例化,最终返回给调用者一个可直接使用的对象,这一切对于调用者是透明的,这就是 IOC 中的“控制”
// 手动实例化依赖链
C c = new C();
B b = new B(c);
A a = new A(b);
a.doSomething();
// 使用某种手段自动处理依赖链
Container container = new Container();
A a = container.new("A");
那“控制”中的“某种手段”是什么?IOC 只是提出了这个概念,至于手段是什么?IOC 并不关心,只要能实现它就行
到这就可以来解释 DI 了,依赖注入(DI)就是 IOC 中的“某种手段”,用来解决 IOC 中需要调用者手动 new 依赖链的问题。如上例,DI 实现了一个容器,使用该容器提供的方法实例化对象时,调用者不用关心它的依赖链,DI 是 IOC 的一种实现。
所以,IOC 和 DI 并不是同一个东西,DI 是实现 IOC 常用的一种方式,当然还有其他的方式。
DI 如何实现?说白了就是要分析对象的依赖关系,本质上是分析一个类的构造方法参数(我们使用构造方法传参来实现“反转”),当发现参数是一个对象时,我们继续分析这个参数对象的类,依此类推,递归分析,一层层返回对象实例,最终帮助调用者返回最外层的对象实例。
本质上就是要找到一种 分析类的机制,通常各大语言会提供一种叫做 反射 的机制,使用这种机制我们就能分析一个类的构造方法(当然还可以干更多的事情)
class Container {
public static function new($class) {
// 语言提供的反射类,用于分析一个类
$ref = new ReflectionClass($class);
// 获取 $class 构造方法
$constructor = $ref->getConstructor();
if($constructor != null) {
// 获取构造方法参数
$args = $constructor->getParameters();
foreach($args as $k => $arg){
if(is_object($arg)){ // 如果参数是一个对象
// 获取依赖对象的类名
$depClass = $arg->getClass();
// 递归分析
$tmp = $this->get($depClass->name);
// 逐层返回对象实例,直至最外层
return new($tmp);
}
}
}else{
return new $class;
}
}
}
上例,我们只处理了构造方法注入的简单情况,还没有处理成员属性注入、构造方法普通参数等。真实的 DI(IOC) 会比这更复杂,无非是将上述情况都处理掉,大家可以自行实践。
本文基于个人理解,如果有误请留言,OVER
热门评论
最后一段代码里的 => 和 -> 还有$符号是什么意思