本篇为学习笔记,文中大量内容参考互联网,再加上一些笔者自己的思考。

一. 前言

依赖注入 (DI) 的概念,在工程领域中依赖注入是用于实现解耦的常用手段,控制反转和依赖注入等概念在 Spring 等后端框架中应用非常广泛。在前端领域中使用依赖注入比较著名的是 AngularJS ,依赖注入是 Angular 中的基础概念:
image.png
在一些编辑器代码中我们也常常可以看到 @inject@injectable这样的注解,比如同事自己实现的 power-di 库提供的依赖注入能力。

  1. const context = new IocContext();
  2. @injectable()
  3. class NRService { }
  4. @injectable()
  5. class LITestService {
  6. @inject()
  7. public testService: NRService;
  8. }
  9. const test = context.get(LITestService);

二. 控制反转

1. 控制反转是什么?

根据 Wiki 上的定义:

控制反转(Inversion of Control)是一种面向对象编程中的设计原则,用来降低计算机代码之间的耦合度,其基本思想是:借助于“第三方”实现具有依赖关系的对象之间的解耦。

IOC 不是什么技术,而是一种设计思想,这个第三方我们常常会定义为 Ioc 容器,power-di 中提供 IocContext就是容器的意思,例如下面的代码:

  1. const context = new IocContext();
  2. @injectable()
  3. class NRService {}
  4. @injectable()
  5. class LITestService {
  6. @inject()
  7. public testService: NRService;
  8. }
  9. const test = context.get(LITestService);

2. 什么是耦合?

image.png => image.png
所谓耦合也就是代码相互之间的联系太直接: 假如 obj2 报错,那么整个系统也都报错了,如果两者不这么直接的发生关系,那么相互影响的概率就小了那么多了。另外,这是比较少的模块,常规项目里显然不仅仅是只有这么少,想象一下多个模块的场景。除了耦合之外,不同齿轮之间的依赖关系也是个头疼的问题。

3. 谁控制谁,控制什么

传统程序设计中,我们直接在对象内部通过 new 创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对 象的创建;

  • 谁控制谁?IoC 容器控制了对象
  • 控制什么?主要控制了外部资源获取(不只是对象,还包括比如文件等资源)

image.png

4. 为何是反转

为何是反转,哪些方面反转了?有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;

  • 为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;
  • 哪些方面反转了?依赖对象的获取被反转了

5. 现实例子

控制反转不只是软件工程的理论,在生活中我们也有用到这种思想。再举一个现实生活的例子:海尔公司作为一个电器制商需要把自己的商品分销到全国各地,但是发现,不同的分销渠道有不同的玩法,于是派出了各种销售代表玩不同的玩法,随着渠道越来越多,发现,每增加一个渠道就要新增一批人和一个新的流程,严重耦合并依赖各渠道商的玩法。实在受不了了,于是制定业务标准,开发分销信息化系统,只有符合这个标准的渠道商才能成为海尔的分销商。让各个渠道商反过来依赖自己标准。反转了控制,倒置了依赖

我们把海尔和分销商当作软件对象,分销信息化系统当作 IOC 容器,可以发现,在没有 IOC 容器之前,分销商就像图 1 中的齿轮一样,增加一个齿轮就要增加多种依赖在其他齿轮上,势必导致系统越来越复杂。开发分销系统之后,所有分销商只依赖分销系统,就像图 2 显示那样,可以很方便的增加和删除齿轮上去。


三. 依赖注入

1. 什么是依赖?

依赖项是指某个类执行其功能所需的服务或对象。如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。例如下面类 Human 中用到一个 Father 对象,我们就说类 Human 对类 Father 有一个依赖。

  1. public class Human {
  2. ...
  3. Father father;
  4. ...
  5. public Human() {
  6. father = new Father();
  7. }
  8. }

仔细看这段代码我们会发现存在一些问题:

  1. 如果现在要改变 father 生成方式,如需要用 new Father(String name) 初始化 father,需要修改 Human 代码;
  2. 如果想测试不同 Father 对象对 Human 的影响很困难,因为 father 的初始化被写死在了 Human 的构造函数中;
  3. 如果 new Father() 过程非常缓慢,单测时我们希望用已经初始化好的 father 对象 Mock 掉这个过程也很困难。

2. 依赖注入是什么?

上面将依赖在构造函数中直接初始化是一种 Hard init 方式,弊端在于两个类不够独立,不方便测试。我们还有另外一种 Init 方式,如下:

  1. public class Human {
  2. ...
  3. Father father;
  4. ...
  5. public Human(Father father) {
  6. this.father = father;
  7. }
  8. }

上面代码中,我们将 father 对象作为构造函数的一个参数传入。在调用 Human 的构造方法之前外部就已经初始化好了 Father 对象。像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入

现在我们发现上面 1 中存在的两个问题都很好解决了,简单的说依赖注入主要有两个好处:

  1. 解耦,将依赖之间解耦。
  2. 因为已经解耦,所以方便做单元测试,尤其是 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. 依赖倒置原则:

  1. 上层模块不应该依赖底层模块,它们都应该依赖于抽象。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。
  3. 依赖倒置原则的中心思想是面向接口编程。
  4. 依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。

比如下面的披萨店,直接依赖于所有 Pizza 对象,那么任何 Pizza 具体实现的任何改变都会影响到 PizzaStore。
image.png
=> 依赖倒置后,披萨店依赖抽象类 Ipazz,不直接依赖 Pizza 对象,详见《六大设计原则之依赖倒置原则(DIP)
image.png

2. 好莱坞原则

好莱坞原则:别打电话给我,有事我会打电话给你

好莱坞原则用在系统的高层组件和低层组件之间,低层组件将自己挂钩到系统上,高层组件会来决定什么时候和如何调用低层组件。高层组件对待低层组件的方式是,别来调用我,我会调用你。

有时候依赖倒置原则又被称为好莱坞原则,但还是有一些不同。依赖倒置原则更多是说,我们应该面向接口编程;好莱坞原则是说,低层组件将自己挂钩到系统上,由系统来主动调用。

四. 使用案例

我们以 AngularJS 的例子举例:

1. 创建可注入的服务

我们先创建可注入的服务,比方说我们创建一个 HeroService ,通过 @injectable 注解注册该服务。这个服务提供 getHeros 方法可以获取当前的 heros 对象。

  1. import { Injectable } from '@angular/core';
  2. import { HEROES } from './mock-heroes';
  3. @Injectable({
  4. providedIn: 'root',
  5. })
  6. export class HeroService {
  7. getHeroes() { return HEROES; }
  8. }

2. 注入服务

在 AngularJS 中将依赖项注入组件的 constructor() 中,提供具有此依赖项类型的构造函数参数即可,可以看出来 Angular 是在构造器中进行自动注入的。

  1. constructor(heroService: HeroService)

五. 如何实现依赖注入?

在 C#/Java 下,一般依赖注入的流程是:
image.png

在 java 中是使用的 “反射” 原理,反射就是 Reflection,Java 的反射是指程序在运行期可以拿到一个对象的所有信息。在 power-di 中也看到了相关的依赖:
image.png
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 容器配置依赖对象”。


七. 参考文章