手把手教你实现TypeScript下的IoC容器

在此篇文章开始以前,先向你们简单介绍 IoC。什么是 IoC?以及为何咱们须要 IoC?以及本文核心,在 TypeScript 中实现一个简单的 IoC 容器?node

IoC 定义

咱们看维基百科定义:git

控制反转(Inversion of Control,缩写为 IoC),是面向对象编程中的一种设计原则,能够用来减低计算机代码之间的耦合度。其中最多见的方式叫作依赖注入(Dependency Injection,简称 DI),还有一种方式叫“依赖查找”(Dependency Lookup)。经过控制反转,对象在被建立的时候,由一个调控系统内全部对象的外界实体,将其所依赖的对象的引用传递(注入)给它。 ———— 维基百科es6

简单来讲,IoC 本质上是一种设计思想,能够将对象控制的全部权交给容器。由容器注入依赖到指定对象中。由此实现对象依赖解耦。github

初识 Container

假设咱们有三个接口: 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();
  }
}
复制代码

@inject

能够发现。咱们在 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 }
复制代码

Container

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,本身的乐趣和收获也不少~

以上,对你们若有助益,不胜荣幸。

相关文章
相关标签/搜索