在此篇文章開始以前,先向你們簡單介紹 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,本身的樂趣和收穫也不少~
以上,對你們若有助益,不勝榮幸。