前言
之前对SOILD原则中的IOC(控制反转)的概念有过一定了解,利用IOC我们可以很轻松的实现模块间的接藕,但理论上的知识都比较晦涩,一直不知道怎么实际应用。由于theia中深入依赖了一个利用DI(依赖注入)来实现IOC的库--InversifyJs,所以借着阅读theia源码的机会,深入了解了一番。
本系列会分为上、下两个章节介绍:上篇中会介绍inversifyjs、下篇中会介绍theia中对inversifyJs的使用(WIP)。
- 希望能帮助大家理解
IOC的概念,并能对inversifyjs库有一定的理解和应用。 - 因为
inversifyjs的使用也导致theia源码有些晦涩难读,希望通过本系列介绍,能帮助大家更轻松的阅读theia源码。
什么是IOC
在代码结构设计中,保证高内聚、低耦合是非常重要的,不仅仅是开发效率,包括后续的代码维护扩展效率都会收到很大的影响,尤其是在大型代码架构中,如果模块间耦合度过高,修改往往是牵一发而动全身。
在面向对象程序设计中,我们在使用一个类功能是要先将类进行实例化,业务庞杂的功能是通过类之间的相互调用相互依赖完成的,在常规的面向对象操作中,我们往往会将依赖对象的实例化工作放在类内进行,但这就违反了单一职责原则----该类除了本身的职责外,还要负责依赖对象的实例化工作。
而在IOC中,用第三方容器来处理对象的实例化过程。当类需要使用某个对象时,类会自动创建或获取一个实例注入到类当中,这样类与类之间没有了直接关联。对象创建和使用的控制权从类转移到了容器, 即所谓控制反转。
举个简单的例子:中午想吃午饭,有两种选择:自己做或者点外卖。如果你自己做的话,柴米油盐酱醋都需要备齐,缺了调料或者原材料你还要自己现买。如果点外卖的话,你只需要告诉商家你想要什么菜,什么口味,外卖到了你负责吃就可以了。
InversifyJs
InversifyJs是基于TS利用依赖注入实现IOC的轻量工具库。通过装饰器+Reflect Metadata实现元数据的注入和读取。
Reflect MetaData
在像依赖注入这种模式下,我们会有这样的需求:在保持原有class一致性的前提下,可以为class添加metadata(元数据),并可以方便的读取metadata,在这个过程中不会对class的结构产生任何影响。 我们会想到用装饰器+反射来注入metadata,然后通过反射来获取metadata。
ES6规范中新增了Reflectapi,这api其实是将在es6前的一些用于元编程的api放在了统一的命名空间下,但是遗憾的是还没有Reflect Metadata的规范,而且ecma262的装饰器提案现在还在stage2阶段。
不过ts中已经实现了完备的的装饰器能力,并实现了Reflect Metadata,通过这个能力,我们可以结合装饰器很轻松的定义并读取元数据,具体的api不赘述了。有一个比较巧妙的点是,Reflect Metadata是利用了WeakMap来实现的元数据存储,可以在不增加引用计数的情况下,把对象作为key,元数据集合作为value,感兴趣的阔以看源码实现。
InversifyJS 基本使用
先拿官方的例子简化版来介绍一下基本使用。
先定义接口:
// file interfaces.ts
export interface Warrior {
fight(): string;
}
export interface Weapon {
hit(): string;
}
下面的类型定义用作InversifyJs中的模块标识符:
// file types.ts
cosnt TYPES = {
Warrior: Symbol.for("Warrior"),
Weapon: Symbol.for("Weapon"),
}
在IOC中有一条规定:高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。比方说下面的Ninja中,仅仅依赖了Weapon这个抽象接口,而并不直接依赖于Katana,这个接口在ts中只用于类型校验,在编译为js后就消失了。
在InversifyJs中我们用injectable装饰器来标志一个类是否是可依赖注入的,用inject来标志要注入的依赖类型,在解析阶段的时候会根据inject的参数在容器中查找依赖并注入。
// file entities.ts
import { injectable, inject } from "inversify";
import "reflect-metadata";
import { Weapon, Warrior } from "./interfaces";
import { TYPES } from "./types";
@injectable()
class Katana implements Weapon {
public hit() {
return "cut!";
}
}
@injectable()
class Ninja implements Warrior {
private _katana: Weapon;
public constructor(
@inject(TYPES.Weapon) katana: Weapon,
) {
this._katana = katana;
}
public fight() { return this._katana.hit(); }
}
export { Ninja, Katana, Shuriken };
然后我们创建一个容器,并将我们的类绑定到容器上。这里也是唯一耦合的地方。
绑定之后,当我们通过标识符从容器中get实例时,InversifyJs会自动解析整个依赖,并返回给我们正确的实例。
// file container.ts
import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon } from "./interfaces";
import { Ninja, Katana } from "./entities";
const myContainer = new Container();
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja);
myContainer.bind<Weapon>(TYPES.Weapon).to(Katana);
// resolve dependencies
const ninja = myContainer.get<Warrior>(TYPES.Warrior);
ninja.fight() // cut!
Container
InversifyJs的实现离不开Container的概念,Container可以看作是整个依赖注入链路的入口。这里先简单介绍一下容器相关常用的API。
ContainerModule
在像theia这样的大型项目中,如果我们全部的依赖都直接绑定在Container上,显然不那么美观。而ContainerModule则是用于管理众多绑定的方法。
通过ContainerModule,我们就可以把绑定分散到不同的模块中,可以使架构条理更清晰。
const module = new ContainerModule((bind) => {
bind(TYPES.Warrior).to(Ninja);
})
container.load(module)
ContainerModule的构造函数只有一个registry的方法回调参数,该方法提供了一些容器的基本工具方法:bind(绑定)、unbind(解除绑定)、isbound(判断是否绑定)、rebind(重新绑定)等。
源码链接
container.bind
通过bind方法,会根据传入的标志符号生成一个新的binding对象,用于记录bind相关的信息,这个binding对象会存储在container的一个字典集中。在这个时候还没有真正绑定内容,bind方法会返回一个链式调用对象用于处理绑定相关操作。
const bindingToSyntax = myContainer.bind<Warrior>(TYPES.Warrior)// 这时还没有真正绑定
这里的链式调用主要是有四个部分:
BindingToSyntax、BindingInSyntax、BindingWhenSyntax、BindingOnSyntax
// myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja).inSingletonScope();
const bindingInWhenOnSyntax = bindingToSyntax.to(Ninja)//这里才真正绑定
bindingInWhenOnSyntax.inSingletonScope(); // 后面可继续链式处理in、when、on
BindingToSyntax
这个对象定义了丰富的绑定函数,这里简单介绍几个。源码链接
to:绑定一个类(获取类的实例)toSelf:to的简化版,当serviceIdentifier(标识符)是构造函数时,直接绑定自身。
bind(Ninja).toSelf();
toConstantValue:绑定一个常量toDynamicValue:绑定为动态数值,解析时执行传入的函数获取依赖。
bind(TYPES.Warrior).toDynamicValue(() => new Ninja());
toFactory:绑定为工厂方法toAutoFactory:绑定为自动工厂方法,工厂方法自动生成为获取传入的serviceIdentifier的依赖注入。源码链接
toProvider:绑定为异步工厂方法。toService:传递绑定,属于一个语法糖,和toDynamicValue的关系与toFactory和toAutoFactory之间的关系有点像。reason
BindingInSyntax
这个对象用于设置依赖项作用域。在InversifyJs中一共有三种作用域:
inTransientScope(瞬时作用域模式)每次获取都是新的(也是container的默认依赖项作用域)。inSingletonScope(单例作用域模式)创建的是覆盖类型绑定完整生命周期的单例。这意味着当我们使用container.unbind取消类型绑定时,inSingletonScope会在内存中清除。inRequestScope(请求作用域模式)创建的单例覆盖的是调用container.get、container.getTagged或container.getNamed时的完整生命周期。对这些方法的每一次调用都将解析一个根依赖项及其所有子依赖项。InversifyJS 在planning阶段内部创建了一个名为依赖关系图(具体见下文planning介绍)。inRequestScope作用域会对其中多次出现的对象使用单个实例。这样就减少了所需的解析数,并且在某些情况下可以用作性能优化的选项。
BindingWhenSyntax
这个对象主要用于设置绑定条件,会在planning阶段找activeBindings时进行解析(见下文)。在theia中我们有用到whenTargetNamed、whenTargetIsDefault等方法。
myContainer.bind<Warrior>(TYPES.Warrior).to(Ninja).whenTargetNamed('ninja');
myContainer.bind<Warrior>(TYPES.Warrior).to(Knight).whenTargetNamed('knight');
@injectable()
class Use{
@inject(TYPES.Warrior) @named('ninja')
pirvate _warrior: Warrior; // Ninja
}
BindingOnSyntax
这个对象主要用于设置binding的onActivation钩子,设置后会在Activation阶段执行(见下文)。源码链接
分层依赖注入
指容器之间的父子关系,在planning阶段getBinding时会从当前容器向上递归查找第一个能解析依赖注入的容器。
const parentContainer = new Container();
// 可以通过createChild构建父子关系
const child = parentContainer.createChild()
// 也可以通过改变parent引用构建父子关系
const childContainer = new Container();
childContainer.parent = parentContainer
InversifyJs 工作流
InversifyJS的工作流程主要分为五个阶段:
Annotation- 打标签阶段Planning- 规划阶段Middleware-(optional)中间件阶段.Resolution- 解析阶段Activation-(optional)激活阶段。
Annotation
这个阶段主要是通过decorator给需要注入的对象加标签做标记,下面是InversifyJs中常用的标签装饰器:
injectable: 用于标记类是可以依赖注入的inject: 标记依赖注入对象的serviceIdentifiermultiInject: 多绑定的依赖注入,获取的值为数组named: 具名绑定optional: 标明可选依赖,未找到绑定时为undefined,可声明默认值postConstruct: 类中该标签标注的方法,会在constructor执行后立即执行,同一个类中只能有一个该标签unmanaged用于解除“派生类的构造函数参数的数量必须 >= 其基类的构造函数参数数量”限制
在打标签阶段,会根据不同的标签能力生成不同的metaData,所有的metaData都会挂载在当前类上。每个类上可能会有多种类型的metaData,下面介绍常见的几种:
METADATA_KEY.PARAM_TYPES: 通过injectable注入,用于标记类是可以依赖注入的,同时标记出构造函数参数。METADATA_KEY.POST_CONSTRUCT: 通过@postConstruct注入到类方法上,该方法会在类实例初始化后立即执行。METADATA_KEY.TAGGED:标记类上的构造函数参数的元数据。(value: Map<string, MetaData[]>,其中的key为参数的索引)METADATA_KEY.TAGGED_PROP:标记类上的属性元数据。(value: Map<string, MetaData[]>,其中key为类属性名称)METADATA_KEY.TAGGED和METADATA_KEY.TAGGED_PROP的元数据值为是Map<string, MetaData[]>,key是参数index或者属性名称。 value中每个MetaData标记了该参数或属性上的一个装饰器(named、inject、multiInject、optional...)。(可通过该链接查看打标签结果)
Planning
planning阶段主要干了三件事源码链接:
- 创建
context:context是一个上下文对象,是当前信息的集合体 - 创建
target:target主要描述一个需要解析依赖的"待注入对象",Target 有三种类型:
ConstructorArgument代表构造函数参数注入的依赖ClassProperty代表类属性注入的依赖Variable代表通过container.get方法获取的对象 源码链接
- 创建
Requset树:createSubRequests这个函数就是从上到下创建Request依赖树的过程,最后每一个依赖我们都会有一个Request。
- 整个request创建过程是一个递归处理的过程。源码链接
- 第一次执行的时候,会创建一个
plan,并与context进行绑定。同时该plan也会指向rootRequest。 activeBindings是在所有该serviceIdentifier的binding中。根据target的name、tag等标签进行筛选的集合。
- 在对activeBindings遍历处理的过程中分为
single-inject和multi-inject两个情况single-inject:@inject或者container.get获取multi-inject:@multiInject或者container.getAll获取 源码链接
下面是Request的数据结构,其中的requestScope就是为了上文提到的inRequestScope (请求作用域模式)
源码链接
Planning阶段产物如下图所示:
Middleware
当我们运行container.get(...)等方法获取注入对象时,实际上就是执行planning和 resolution的过程。而在planning和resolution之间有一个阶段:Middleware。
// eg: 定义一个console中间件
const consoleMiddleWare = function(next: inversify.interfaces.Next): inversify.interfaces.Next {
return (args: inversify.interfaces.NextArgs) => {
let nextContextInterceptor = args.contextInterceptor;
// 重定义劫持器
args.contextInterceptor = (context: inversify.interfaces.Context) => {
console.log(context); // 输出context
return nextContextInterceptor(context)
}
return next(args);
}
}
// 注入中间件
container.applyMiddleWare(consoleMiddleWare)
中间件的注入过程是一个函数的Composite过程,多个中间件应用时,会从container的_planAndResolve方法开始一层一层的合成。
源码链接
当获取注入对象时,如果先前有注入中间件,就会执行合成后的中间件方法。源码链接
在执行到
_planAndResolve时,当处理完planning阶段获取到上下文后,可以通过中间件重写拦截器contextInterceptor,劫持context并实现一些劫持方法源码链接(eg:inversify-logger-middleware)
Resolution
这一阶段主要是从Planning阶段生成的context开始,将Request从下到上逐一解析并初始化实例、拼装实例、并最终的到想要的类的实例的过程。(相关源码)下面是Resolution阶段的大致流程图:
Activation
当一个依赖被解析之后,并且在它被加入到缓存(如果是SingletonScope或者RequestScope)并注入之前(也就是返回结果之前)会触发Activation。(注:如果是从cache中获取的依赖,不会触发Activation)
这个特性允许开发者做一些别的事情,比如注入一个代理以拦截注入对象的方法或属性的调用。源码链接