在此篇文章开始以前,先向你们简单介绍 IoC。什么是 IoC?以及为何咱们须要 IoC?以及本文核心,在 TypeScript 中实现一个简单的 IoC 容器?node
咱们看维基百科定义:git
控制反转(Inversion of Control,缩写为 IoC),是面向对象编程中的一种设计原则,能够用来减低计算机代码之间的耦合度。其中最多见的方式叫作依赖注入(Dependency Injection,简称 DI),还有一种方式叫“依赖查找”(Dependency Lookup)。经过控制反转,对象在被建立的时候,由一个调控系统内全部对象的外界实体,将其所依赖的对象的引用传递(注入)给它。 ———— 维基百科es6
简单来讲,IoC 本质上是一种设计思想,能够将对象控制的全部权交给容器。由容器注入依赖到指定对象中。由此实现对象依赖解耦。github
假设咱们有三个接口: Warrior 战士、Weapon 武器、ThrowableWeapon 投掷武器。编程
export interface Warrior {
fight(): string;
sneak(): string;
}
export interface Weapon {
hit(): string;
}
export interface ThrowableWeapon {
throw(): string;
}
复制代码
对应分别有实现这三个接口的类:Katana 武士刀、Shuriken 手里剑、以及 Ninja 忍者。json
export class Katana implements Weapon {
public hit() {
return 'cut!';
}
}
export class Shuriken implements ThrowableWeapon {
public throw() {
return 'hit!';
}
}
export class Ninja implements Warrior {
private _katana: Weapon;
private _shuriken: ThrowableWeapon;
public constructor() {
this._katana = new Katana();
this._shuriken = new Shuriken();
}
public fight() {
return this._katana.hit();
}
public sneak() {
return this._shuriken.throw();
}
}
复制代码
由上面的示例,很明显咱们能够得知,Ninja 类依赖了 Katana 类和 Shuriken 类。这种依赖关系对于咱们来讲很常见,可是随着应用的日益迭代,愈来愈复杂的状况下,类与类之间的耦合度也会愈来愈高,应用会变得愈来愈难以维护。数组
对于上述 Ninja 类来讲,如若往后须要不断新增其余武器对象,甚至忍术对象,这个 Ninja 类文件会引入愈来愈多的对象,Ninja 类也会愈来愈臃肿。若是一个应用内部每个类都对彼此产生依赖,可能代码写到后面就是沉重的技术债了。bash
所以 IoC 的思想的出现,就是为了实现对象依赖解耦。ide
那么先带你们简单认识 IoC 容器的使用。函数
const container = new Container();
const TYPES = {
Warrior: Symbol.for('Warrior'),
Weapon: Symbol.for('Weapon'),
ThrowableWeapon: Symbol.for('ThrowableWeapon')
};
container.bind<Weapon>(TYPES.Weapon).to(Katana);
container.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
container.bind<Warrior>(TYPES.Warrior).to(Ninja);
const ninja = container.get<Ninja>(TYPES.Warrior);
ninja.fight(); // "cut!"
ninja.sneak(); // "hit!"
复制代码
上面的 Container 实际上接手了对象依赖的管理,使得 Ninja 类脱离了对 Katana 类和 Shuriken 类的依赖!
此时 Ninja 类只依赖抽象的接口(Weapon、ThrowableWeapon)而不是依赖具体的类(Katana、Shuriken)。
那么 Container 怎样作到的呢?它的实现原理又是怎样的呢?是否是很好奇?其实没有什么黑魔法,接下来就会为你们揭开 Container 实现 IoC 原理的神秘面纱。
首先咱们先将 Ninja 类改写以下:
export class Ninja implements Warrior {
private _katana: Weapon;
private _shuriken: ThrowableWeapon;
public constructor(@inject(TYPES.Weapon) katana: Weapon, @inject(TYPES.ThrowableWeapon) shuriken: ThrowableWeapon) {
this._katana = katana;
this._shuriken = shuriken;
}
public fight() {
return this._katana.hit();
}
public sneak() {
return this._shuriken.throw();
}
}
复制代码
能够发现。咱们在 Ninja 类的构造函数里对每一个参数进行了 @inject 装饰器声明。那么这个@inject 又干了什么事情?。@inject 也不过是咱们实现的一个装饰器函数而已,代码以下:
export function inject(serviceIdentifier: string | symbol) {
return function(target: any, propertyKey: string, parameterIndex: number) {
const metadata = {
key: 'inject.tag',
value: serviceIdentifier
};
Reflect.defineMetadata(`custom:paramtypes#${parameterIndex}`, metadata, target);
};
}
复制代码
这里出现了 Reflect.defineMetadata,你们可能比较陌生。Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。
提案文档: Metadata Proposal - ECMAScript。
想要使用此特性,须要安装 reflect-metadata 这个包,同时配置 tsconfig 以下:
{
"compilerOptions": {
"target": "es5",
"lib": ["es6", "DOM"],
"types": ["reflect-metadata"],
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
复制代码
在这个场景下,我为 @inject 对象里每一个传入的参数自定义了 metadataKey。好比在上述@inject(TYPES.Weapon)中,target 就是 Ninja 类,parameterIndex 就是 0。@inject(TYPES.ThrowableWeapon)一样道理。
所以在 Ninja 类里,根据@inject 装饰器的声明,在运行时给 Ninja 类添加了两个元数据。
custom:paramtypes#0 -> { key: "inject.tag", value: TYPES.Weapon }
custom:paramtypes#1 -> { key: "inject.tag", value: TYPES.ThrowableWeapon }
复制代码
IoC 容器的主要功能是什么呢?
如下是一个十分简单的 Container 容器实现代码。
type Constructor<T = any> = new (...args: any[]) => T;
class Container {
bindTags = {};
bind<T>(tag: string | symbol) {
return {
to: (bindTarget: Constructor<T>) => {
this.bindTags[tag] = bindTarget;
}
};
}
get<T>(tag: string | symbol): T {
const target = this.bindTags[tag];
const providers = [];
for (let i = 0; i < target.length; i++) {
const paramtypes = Reflect.getMetadata('custom:paramtypes#' + i, target);
const provider = this.bindTags[paramtypes.value];
providers.push(provider);
}
return new target(...providers.map(provider => new provider()));
}
}
复制代码
bind 方法,主要将全部绑定在容器上依赖创建映射关系。好比如下代码:
const container = new Container();
container.bind<Weapon>(TYPES.Weapon).to(Katana);
container.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);
container.bind<Warrior>(TYPES.Warrior).to(Ninja);
复制代码
建立容器后,经过 bind 绑定了三个对象,所以容器中造成了(bindTags)如下这样的关系。
{
[TYPES.Weapon]: Katana,
[TYPES.ThrowableWeapon]: Shuriken,
[TYPES.Warrior]: Ninja
}
复制代码
绑定依赖对象后,咱们再结合实例看容器的 get 方法:
const ninja = container.get<Ninja>(TYPES.Warrior);
复制代码
容器的 get 方法经过 tag 参数在 bingTags 映射里,找到目标对象,对应到上述代码也就是,找到了 Ninja 类。
紧接着重头戏来了,咱们能够经过 target.length(也就是 function.length)得知 Ninja 类构造函数的参数数量,声明了 providers 数组用于存储 Ninja 类的依赖。还记得一开始咱们经过 @inject 在类上添加的两个元数据。此时发挥了重要做用!所以经过元数据便可查找到依赖。
以下:
const paramtypes = Reflect.getMetadata('custom:paramtypes#' + i, target);
const provider = this.bindTags[paramtypes.value];
复制代码
第一个参数对应 custom:paramtypes#0,paramtypes.value 即为 TYPES.Weapon,此时在 bindTags 查到,找到了 Katana 类依赖!
同理第二个参数也找到了 Shuriken 类依赖。
找到全部在构造函数中声明的依赖后,真正开始注入依赖,以下。
return new target(...providers.map(provider => new provider()));
复制代码
所以最后,经过容器 get 方法,成功获得了注入了依赖的 ninja 实例。
ninja.fight(); // "cut!"
ninja.sneak(); // "hit!"
复制代码
噌噌噌!正确运行!
经过容器管理,类真正作到了依赖抽象的接口,而不是依赖具体的类。践行了 IoC 的思想。
不过上文的 IoC 容器,也只是一个小小的玩具,它所产生的意义主要是引导指示的价值。但愿经过此文,可让你们理解和重视 IoC 的使用。固然笔者也是刚刚学习 IoC,业余时间敲下这个 demo,本身的乐趣和收获也不少~
以上,对你们若有助益,不胜荣幸。