十幾行程式碼實現一個ts依賴注入

全菜工程師發表於2019-01-28

專案地址

靈感來自angular和我自己寫的框架indiv

感謝大佬們star使用

github

關於依賴注入

IOC(Inversion of Control)即控制反轉,DI(Dependency Injection)即依賴注入。

假設我們有一個類Human,要例項一個Human,我們需要例項一個類Clothes。而例項化衣服Clothes,我們又需要例項化布Cloth,例項化鈕釦等等。

當需求達到一定複雜的程度時,我們不能為了一個人穿衣服去從布從鈕釦開始從頭實現,最好能把所有的需求放到一個工廠或者是倉庫,我們需要什麼直接從工廠的倉庫裡面直接拿。

這個時候就需要依賴注入了,我們實現一個IOC容器(倉庫),然後需要衣服就從倉庫裡面直接拿例項好的衣服給人作為屬性穿上去。

IOC是一種很好的解耦合思想,在開發中,IoC意味著你設計好的物件交給容器控制,而不是使用傳統的方式,在物件內部直接控制。在軟體開發中有很好的作用,不僅被應用在JavaEE裡,在其它語言裡同樣適用。

其實總結起來大概就是你需要我,工廠就把我打扮好派我去找你。

實現一個IOC容器

因為key型別不一定是字元型,而且陣列遍歷比較浪費效能,因此不選擇Object和Array而選擇Map作為容器的資料結構。

因為目標是實現一個可以懶漢模式的IOC容器,所以可以在類Injector裡寫了兩個私有類,一個存token和依賴,一個存token和例項化的依賴例項。

在需要某個依賴例項的時候,先去例項的Map中根據token查詢出對應的token的例項,如果沒有就從存放依賴類的Map中拿出來例項化之後再放入存放例項的Map。

export class Injector {
  private readonly providerMap: Map<any, any> = new Map();
  private readonly instanceMap: Map<any, any> = new Map();
  public setProvider(key: any, value: any): void {
    if (!this.providerMap.has(key)) this.providerMap.set(key, value);
  }
  public getProvider(key: any): any {
    return this.providerMap.get(key);
  }
  public setInstance(key: any, value: any): void {
    if (!this.instanceMap.has(key)) this.instanceMap.set(key, value);
  }
  public getInstance(key: any): any {
    if (this.instanceMap.has(key)) return this.instanceMap.get(key);
    return null;
  }
  public setValue(key: any, value: any): void {
    if (!this.instanceMap.has(key)) this.instanceMap.set(key, value);
  }
}
複製程式碼

現在new一下,這個容器就建立出來了。

export const rootInjector = new Injector();
複製程式碼

實現服務

偷偷借走了ng的Injectable,通過類裝飾器把類存入容器。

export function Injectable(): (_constructor: any) => any {
  return function (_constructor: any): any {
      rootInjector.setProvider(_constructor, _constructor);
      return _constructor;
  };
}
複製程式碼

把類作為token,把該類存入provider容器,提供給需要依賴的類。

實現基於註解的屬性注入

之所以不實現構造注入,setter注入是因為像ng和react這類框架會對直接對建構函式進行注入或是限制建構函式的引數,為了儘量跨框架跨前後端使用,所以還是用裝飾器對屬性注入儘量減少侵入性。

屬性裝飾器和反射能幫我們實現這一功能。

export function Inject(): (_constructor: any, propertyName: string) => any {
  return function (_constructor: any, propertyName: string): any {
    const  propertyType: any = Reflect.getMetadata('design:type', _constructor, propertyName);
    const injector: Injector = rootInjector;

    let providerInsntance = injector.getInstance(propertyType);
    if (!providerInsntance) {
        injector.getProvider(propertyType);
        providerInsntance = new providerClass();
        injector.setInstance(key, providerInsntance);
    }
    _constructor[propertyName] = providerInsntance;

    return (_constructor as any)[propertyName];
  };
}
複製程式碼

使用Reflect的後設資料 Reflect.getMetadata('design:type') 獲取屬性的型別,並作為token去 injector.getInstance 查詢對應的例項,如果有則直接將屬性對映為查詢到的例項。這樣就保證我們每次使用裝飾器的屬性都會獲得單例。

如果沒有查詢到則去另外一個Map中拿出依賴並例項化存入例項的Map。因為js單執行緒懶漢模式不存線上程安全這麼一說,所以沒有選擇初始化時就把全部的依賴例項化。

demo

import { Inject, Injectable } from '../injector';

@Injectable()
class Cloth {
    public name: string = '麻布';
}

@Injectable()
class Clothes {
  @Inject() public cloth: Cloth;
}

class Human {
  @Inject() public clothes: Clothes;
}

const pepe = new Human();
console.log(pepe);
// {
//   clothes: {
//      cloth: {
//        name: '麻布'
//      }
//   }
//}
複製程式碼

最後我們可以直接new pepe,不需要關心pepe穿的衣服 Clothes 和衣服需要的布料 Cloth

最後推薦結合rxjs做響應式程式設計。ng大法好!

相關文章