天天看點

基于 Typescript 和 Decorator 實作依賴注入

什麼是依賴注入

依賴注入是将一個對象所依賴的其他對象直接提供給這個對象,而不是在目前對象中直接建構這些依賴的對象。

為什麼要使用依賴注入

  • 便于單元測試
  • 解耦,統一管理被依賴對象的執行個體化,不用在類的内部建立被依賴對象

如何實作依賴注入

Typescript 中的裝飾器 Decorator

裝飾器是一種特殊類型的聲明,它能夠被附加到類聲明,方法,通路符,屬性或參數上。 裝飾器使用@expression這種形式,expression求值後必須為一個函數,它會在運作時被調用,被裝飾的聲明資訊做為參數傳入。例如,有一個@sealed裝飾器,我們會這樣定義和使用sealed函數:

function sealed(target) {
    // do something with "target" ...
}

@sealed
class MyClass {}           

裝飾器工廠

裝飾器工廠就是一個簡單的函數,它傳回一個裝飾器。

我們可以通過下面的方式來寫一個裝飾器工廠函數:

function color(value: string) { // 這是一個裝飾器工廠
    return function (target) { //  這是裝飾器
        // do something with "target" and "value"...
    }
}           

類裝飾器

類裝飾器在類聲明之前被聲明(緊靠着類聲明)。 類裝飾器應用于類構造函數,可以用來監視,修改或替換類定義。類裝飾器表達式會在運作時當作函數被調用,類的構造函數作為其唯一的參數。如果類裝飾器傳回一個值,它會使用提供的構造函數來替換類的聲明。

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}           

方法、屬性、通路器的裝飾器

方法、屬性、通路器裝飾器表達式會在運作時當作函數被調用,傳入下列3個參數:

  1. 對于靜态成員來說是類的構造函數,對于執行個體成員是類的原型對象。
  2. 成員的名字。
  3. 成員的屬性描述符。

對于屬性裝飾器,第3個參數為 undefined。

Typescript 中的 Reflect Metadata

Reflect Metadata 是 ES7 的一個提案,它主要用來在聲明的時候添加和讀取中繼資料。TypeScript 在 1.5+ 的版本已經支援它,要使用 reflect metadata,你需要:

  • npm i reflect-metadata --save

  • tsconfig.json

    裡配置

    emitDecoratorMetadata

    選項為 true
{
  "compilerOptions": {
    "target": "ES2015",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}           

Reflect Metadata 的 API 可以用于類或者類的屬性上,其聲明如下:

function metadata(
  metadataKey: any,
  metadataValue: any
): {
  (target: Function): void;
  (target: Object, propertyKey: string | symbol): void;
};           

Reflect.metadata 可以當作 Decorator 使用,當修飾類時,在類上添加中繼資料,當修飾類屬性時,在類原型的屬性上添加中繼資料,如:

@Reflect.metadata('inClass', 'A')
class Test {
  @Reflect.metadata('inMethod', 'B')
  public hello(): string {
    return 'hello world';
  }
}

console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'           

Reflect metadata 有強大的功能,包括擷取類類型資訊和自定義中繼資料資訊以及擷取自定義中繼資料資訊。

擷取類型資訊

function Prop(): PropertyDecorator {
  return (target, key: string) => {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`); // Aprop type: string
  };
}

class SomeClass {
  @Prop()
  public Aprop!: string;
}           

在裝飾器函數中可以通過下列三種内置的 metadataKey 擷取類型資訊。

  • design:type: 屬性類型
  • design:paramtypes: 參數類型
  • design:returntype: 傳回值類型

自定義 metadataKey

Reflect Metadata 除能擷取内置類型資訊外,還可用于自定義 metadataKey,并在合适的時機擷取它的值,示例如下:

function classDecorator(): ClassDecorator {
  return target => {
    // 在類上定義中繼資料,key 為 `classMetaData`,value 為 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}

function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在類的原型屬性 'someMethod' 上定義中繼資料,key 為 `methodMetaData`,value 為 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'           

基于 typescript 實作依賴注入以及 Controller Get 裝飾器

在前面我們介紹了 typescript 中的 decorator 和 reflect-metadata。這些都是為實作依賴注入做的基礎準備,下面将介紹如何基于以上技術實作依賴注入。以及基于 decorator 實作 node web 架構中的 Controller Get 等裝飾器。

通過構造函數注入

import 'reflect-metadata';

type Constructor<T=any> = new (...args: any[]) => T;

const Injectable = (): ClassDecorator => target => { };

class OtherService {
  a = 1;
}

// 通過構造函數注入
@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) { }

  testMethod() {
    console.log(this.otherService);
  }
}

const Factory = <T>(target: Constructor<T>): T => {
  // 擷取所有注入的服務
  const providers = Reflect.getMetadata('design:paramtypes', target);
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // OtherService {a: 1}           

上面是一個簡單的通過構造函數實作依賴注入的例子。這裡可能會有一個疑問,裝飾器 Injectable() 似乎什麼都沒做。但是,把 Injectable() 裝飾器去掉後,我們就無法實作依賴注入了。原因是什麼呢?我們可以去編譯後的代碼看下:

// 通過構造函數注入
var TestService = /** @class */ (function () {
    function TestService(otherService) {
        this.otherService = otherService;
    }
    TestService.prototype.testMethod = function () {
        console.log(this.otherService);
    };
    TestService = __decorate([
        Injectable(),
        __metadata("design:paramtypes", [OtherService])
    ], TestService);
    return TestService;
}());           

隻有添加了 Injectable() 裝飾器後才有下面這段代碼,這段代碼将構造函數的參數類型資訊存儲到了 metadata 中,使得之後在執行個體化時能夠擷取到構造函數參數的類型。

TestService = __decorate([
    Injectable(),
    __metadata("design:paramtypes", [OtherService])
], TestService);           

通過類成員方法參數注入

// 通過類成員方法參數注入
const MethodInjectable = (): MethodDecorator => (
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<any>
) => {
  let method = descriptor.value;
  descriptor.value = function (...args) {
    const providers = Reflect.getMetadata('design:paramtypes', target, propertyKey);
    const providersInsts = providers.map(P => new P());

    return method.apply(this, [...providersInsts, ...args]);
  }
};

class TestParamInjectService {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  @MethodInjectable()
  greet(otherService: OtherService) {
    console.log(otherService);
  }
}

const test = new TestParamInjectService('test');
test.greet();  // OtherService {a: 1}           

在類成員方法參數依賴注入中,我們用到了類成員方法裝飾器中的描述符 descriptor。descriptor.value 為該成員方法的值,我們要修改類成員方法,修改 descriptor.value 即可。

Controller 與 Get 的實作(基于 Decorator)

如果你在使用 TypeScript 開發 Node 應用,例如基于 nestjs 開發 node web 應用,相信你對 Controller、Get、POST 這些 Decorator,并不陌生:

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {

  }
}           
const METHOD_METADATA = 'method';
const PATH_METADATA = 'path';

function Controller(path: string): ClassDecorator {
  return target => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  }
}

const createMappingDecorator = (method: string) => (path: string): MethodDecorator => {
  return (target, key, descriptor) => {
    Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
    Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
  }
}

const Get = createMappingDecorator('GET');
const Post = createMappingDecorator('POST');

@Controller('/test')
class SomeClass {
  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

  @Post('/b')
  somePostMethod() {

  }
}

function isConstructor(f) {
  try {
    new f();
  } catch (err) {
    if (err.message.indexOf('is not a constructor') >= 0) {
      return false;
    }
  }
  return true;
}

function isFunction(functionToCheck) {
  return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);

  // 篩選出類的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
    .filter(item => !isConstructor(item) && isFunction(prototype[item]));
  return methodsNames.map(methodName => {
    const fn = prototype[methodName];

    // 取出定義的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
    return {
      route,
      method,
      fn,
      methodName
    };
  });
}

// 得到一些有用的資訊
Reflect.getMetadata(PATH_METADATA, SomeClass); // '/test'

const routes = mapRoute(new SomeClass());
console.log(JSON.stringify(routes));           

輸出結果如下:

[
    {
        "route": "/a",
        "method": "GET",
        "methodName": "someGetMethod"
    },
    {
        "route": "/b",
        "method": "POST",
        "methodName": "somePostMethod"
    }
]           

通過 Reflect.getMetadata 将類的路由取出。通過 mapRoute 将存儲在成員函數上的路由和方法資訊提取出來,映射成 route,提取出有用的資訊。最後,隻需把 route 相關資訊綁在 express 或者 koa 上就 ok 了。

更多

  • 自動掃描
  • 循環依賴問題的解決

在 java spring 中,完整的依賴注入還需要自動掃描功能,在 spring 應用啟動的時候,會自動掃描@Injectable 并自動完成注入工作,而不需要Factory(TestService)這麼寫了。

在自動掃描的過程中,我們需要注意到不能出現循環依賴,或者在掃描過程中處理掉循環依賴問題。要解決循環依賴問題,可以将服務的依賴關系構造成一個有向圖,具體實作是先将目前的服務推入棧中,再逐層遞歸周遊服務的依賴插入圖中(深度優先周遊)。有向圖中存在環則存在循環依賴。有向圖構造出來之後拿出圖中所有出度構成的依賴數組,因為依賴關系是逐層往上的,即将 A 服務所依賴的其他服務依次執行個體化,最後再執行個體化 A ,一直到全部執行個體化完成為止。

nestjs 源碼研究

在 nestjs 中,大量使用了 decorator 和 reflect metadata,如果要深入研究,那麼可以深入到其源碼中進行分析研究。

參考文獻