手把手教你實現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,本身的樂趣和收穫也不少~

以上,對你們若有助益,不勝榮幸。

相關文章
相關標籤/搜索