本篇为学习笔记,文中大量内容参考互联网,再加上一些笔者自己的思考。
一. 前言
依赖注入 (DI) 的概念,在工程领域中依赖注入是用于实现解耦的常用手段,控制反转和依赖注入等概念在 Spring 等后端框架中应用非常广泛。在前端领域中使用依赖注入比较著名的是 AngularJS ,依赖注入是 Angular 中的基础概念:
在一些编辑器代码中我们也常常可以看到 @inject
和 @injectable
这样的注解,比如同事自己实现的 power-di 库提供的依赖注入能力。
const context = new IocContext();
@injectable()
class NRService { }
@injectable()
class LITestService {
@inject()
public testService: NRService;
}
const test = context.get(LITestService);
二. 控制反转
1. 控制反转是什么?
根据 Wiki 上的定义:
控制反转(Inversion of Control)是一种面向对象编程中的设计原则,用来降低计算机代码之间的耦合度,其基本思想是:借助于“第三方”实现具有依赖关系的对象之间的解耦。
IOC 不是什么技术,而是一种设计思想,这个第三方我们常常会定义为 Ioc 容器,power-di 中提供 IocContext
就是容器的意思,例如下面的代码:
const context = new IocContext();
@injectable()
class NRService {}
@injectable()
class LITestService {
@inject()
public testService: NRService;
}
const test = context.get(LITestService);
2. 什么是耦合?
=>
所谓耦合也就是代码相互之间的联系太直接: 假如 obj2 报错,那么整个系统也都报错了,如果两者不这么直接的发生关系,那么相互影响的概率就小了那么多了。另外,这是比较少的模块,常规项目里显然不仅仅是只有这么少,想象一下多个模块的场景。除了耦合之外,不同齿轮之间的依赖关系也是个头疼的问题。
3. 谁控制谁,控制什么
传统程序设计中,我们直接在对象内部通过 new
创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对 象的创建;
- 谁控制谁?IoC 容器控制了对象;
- 控制什么?主要控制了外部资源获取(不只是对象,还包括比如文件等资源)。
4. 为何是反转
为何是反转,哪些方面反转了?有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;
- 为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;
- 哪些方面反转了?依赖对象的获取被反转了。
5. 现实例子
控制反转不只是软件工程的理论,在生活中我们也有用到这种思想。再举一个现实生活的例子:海尔公司作为一个电器制商需要把自己的商品分销到全国各地,但是发现,不同的分销渠道有不同的玩法,于是派出了各种销售代表玩不同的玩法,随着渠道越来越多,发现,每增加一个渠道就要新增一批人和一个新的流程,严重耦合并依赖各渠道商的玩法。实在受不了了,于是制定业务标准,开发分销信息化系统,只有符合这个标准的渠道商才能成为海尔的分销商。让各个渠道商反过来依赖自己标准。反转了控制,倒置了依赖。
我们把海尔和分销商当作软件对象,分销信息化系统当作 IOC 容器,可以发现,在没有 IOC 容器之前,分销商就像图 1 中的齿轮一样,增加一个齿轮就要增加多种依赖在其他齿轮上,势必导致系统越来越复杂。开发分销系统之后,所有分销商只依赖分销系统,就像图 2 显示那样,可以很方便的增加和删除齿轮上去。
三. 依赖注入
1. 什么是依赖?
依赖项是指某个类执行其功能所需的服务或对象。如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。例如下面类 Human 中用到一个 Father 对象,我们就说类 Human 对类 Father 有一个依赖。
public class Human {
...
Father father;
...
public Human() {
father = new Father();
}
}
仔细看这段代码我们会发现存在一些问题:
- 如果现在要改变 father 生成方式,如需要用 new Father(String name) 初始化 father,需要修改 Human 代码;
- 如果想测试不同 Father 对象对 Human 的影响很困难,因为 father 的初始化被写死在了 Human 的构造函数中;
- 如果 new Father() 过程非常缓慢,单测时我们希望用已经初始化好的 father 对象 Mock 掉这个过程也很困难。
2. 依赖注入是什么?
上面将依赖在构造函数中直接初始化是一种 Hard init
方式,弊端在于两个类不够独立,不方便测试。我们还有另外一种 Init 方式,如下:
public class Human {
...
Father father;
...
public Human(Father father) {
this.father = father;
}
}
上面代码中,我们将 father 对象作为构造函数的一个参数传入。在调用 Human 的构造方法之前外部就已经初始化好了 Father 对象。像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入。
现在我们发现上面 1 中存在的两个问题都很好解决了,简单的说依赖注入主要有两个好处:
- 解耦,将依赖之间解耦。
- 因为已经解耦,所以方便做单元测试,尤其是 Mock 测试。
3. 控制反转和依赖注入的关系
我们已经分别解释了控制反转和依赖注入的概念。有些人会把控制反转和依赖注入等同,但实际上它们有着本质上的不同。
- 控制反转是一种思想。
- 依赖注入是一种设计模式。
IoC 框架使用依赖注入作为实现控制反转的方式,但是控制反转还有其他的实现方式,例如说 ServiceLocator,所以不能将控制反转和依赖注入等同。
四. 涉及到的设计原则
设计模式的六大原则有:
- Single Responsibility Principle:单一职责原则
- Open Closed Principle:开闭原则
- Liskov Substitution Principle:里氏替换原则
- Law of Demeter:迪米特法则
- Interface Segregation Principle:接口隔离原则
- Dependence Inversion Principle:依赖倒置原则
把这六个原则的首字母联合起来(两个 L 算做一个)就是 SOLID (solid,稳定的),其代表的含义就是这六个原则结合使用的好处:建立稳定、灵活、健壮的设计。
1. 依赖倒置原则:
- 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
- 抽象不应该依赖于细节,细节应该依赖于抽象。
- 依赖倒置原则的中心思想是面向接口编程。
- 依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。
比如下面的披萨店,直接依赖于所有 Pizza 对象,那么任何 Pizza 具体实现的任何改变都会影响到 PizzaStore。
=> 依赖倒置后,披萨店依赖抽象类 Ipazz,不直接依赖 Pizza 对象,详见《六大设计原则之依赖倒置原则(DIP)》
2. 好莱坞原则
好莱坞原则:别打电话给我,有事我会打电话给你。
好莱坞原则用在系统的高层组件和低层组件之间,低层组件将自己挂钩到系统上,高层组件会来决定什么时候和如何调用低层组件。高层组件对待低层组件的方式是,别来调用我,我会调用你。
有时候依赖倒置原则又被称为好莱坞原则,但还是有一些不同。依赖倒置原则更多是说,我们应该面向接口编程;好莱坞原则是说,低层组件将自己挂钩到系统上,由系统来主动调用。
四. 使用案例
我们以 AngularJS 的例子举例:
1. 创建可注入的服务
我们先创建可注入的服务,比方说我们创建一个 HeroService
,通过 @injectable
注解注册该服务。这个服务提供 getHeros
方法可以获取当前的 heros 对象。
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable({
providedIn: 'root',
})
export class HeroService {
getHeroes() { return HEROES; }
}
2. 注入服务
在 AngularJS 中将依赖项注入组件的 constructor()
中,提供具有此依赖项类型的构造函数参数即可,可以看出来 Angular 是在构造器中进行自动注入的。
constructor(heroService: HeroService)
五. 如何实现依赖注入?
在 C#/Java 下,一般依赖注入的流程是:
在 java 中是使用的 “反射” 原理,反射就是 Reflection,Java 的反射是指程序在运行期可以拿到一个对象的所有信息。在 power-di
中也看到了相关的依赖:
Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持它。
六. 总结
1. 控制反转是一种在软件工程中解耦合的思想,调用类只依赖接口,而不依赖具体的实现类,减少了耦合。控制权交给了容器,在运行的时候才由容器决定将具体的实现动态的“注入”到调用类的对象中。
2. 依赖注入是一种设计模式,可以作为控制反转的一种实现方式。依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。
3. 通过 IoC 框架,类 A 依赖类 B 的强耦合关系可以在运行时通过容器建立,也就是说把创建 B 实例的工作移交给容器,类A只管使用就可以。
由于控制反转概念比较含糊, 所以2004(《Inversion of Control Containers and the Dependency Injection pattern》)年大师级人物 Martin Fowler 又给出了一个新的名字:“依赖注入”,相对 IoC 而言,“依赖注入”明确描述了“被注入对象依赖 IoC 容器配置依赖对象”。