了不得的 IoC 與 DI

本文阿寶哥將從六個方面入手,全方位帶你一塊兒探索麪向對象編程中 IoC(控制反轉)和 DI(依賴注入) 的設計思想。閱讀完本文,你將瞭解如下內容:javascript

  • IoC 是什麼、IoC 能解決什麼問題;
  • IoC 與 DI 之間的關係、未使用 DI 框架和使用 DI 框架之間的區別;
  • DI 在 AngularJS/Angular 和 NestJS 中的應用;
  • 瞭解如何使用 TypeScript 實現一個 IoC 容器,並瞭解 裝飾器、反射 的相關知識。

1、背景概述

在介紹什麼是 IoC 容器以前,阿寶哥來舉一個平常工做中很常見的場景,即建立指定類的實例。最簡單的情形是該類沒有依賴其餘類,但現實每每是殘酷的,咱們在建立某個類的實例時,須要依賴不一樣類對應的實例。爲了讓小夥伴們可以更好地理解上述的內容,阿寶哥來舉一個例子。前端

一輛小汽車 🚗 一般由 發動機、底盤、車身和電氣設備 四大部分組成。汽車電氣設備的內部構造很複雜,簡單起見,咱們只考慮三個部分:發動機、底盤和車身。vue

(圖片來源:https://www.newkidscar.com/ve...java

在現實生活中,要造輛車仍是很困難的。而在軟件的世界中,這可難不倒咱們。👇是阿寶哥要造的車子,有木有很酷。node

(圖片來源:https://pixabay.com/zh/illust...react

在開始造車前,咱們得先看一下 「圖紙」:git

看完上面的 「圖紙」,咱們立刻來開啓造車之旅。第一步咱們先來定義車身類:angularjs

1.定義車身類github

export default class Body { }

2.定義底盤類typescript

export default class Chassis { }

3.定義引擎類

export default class Engine {
  start() {
    console.log("引擎發動了");
  }
}

4.定義汽車類

import Engine from './engine';
import Chassis from './chassis';
import Body from './body';

export default class Car {
    engine: Engine;
    chassis: Chassis;
    body: Body;

    constructor() {
      this.engine = new Engine();
      this.body = new Body();
      this.chassis = new Chassis();
    }

    run() {
      this.engine.start();
    }
}

一切已準備就緒,咱們立刻來造一輛車:

const car = new Car(); // 阿寶哥造輛新車
car.run(); // 控制檯輸出:引擎發動了

如今雖然車已經能夠啓動了,但卻存在如下問題:

  • 問題一:在造車的時候,你不能選擇配置。好比你想更換汽車引擎的話,按照目前的方案,是實現不了的。
  • 問題二:在汽車類內部,你須要在構造函數中手動去建立汽車的各個部件。

爲了解決第一個問題,提供更靈活的方案,咱們能夠重構一下已定義的汽車類,具體以下:

export default class Car {
    body: Body;
    engine: Engine;
    chassis: Chassis;
  
    constructor(engine, body, chassis) {
      this.engine = engine;
      this.body = body;
      this.chassis = chassis;
    }

    run() {
      this.engine.start();
    }
}

重構完汽車類,咱們來從新造輛新車:

const engine = new NewEngine();
const body = new Body();
const chassis = new Chassis();

const newCar = new Car(engine, body, chassis);
newCar.run();

此時咱們已經解決了上面提到的第一個問題,要解決第二個問題咱們要來了解一下 IoC(控制反轉)的概念。

2、IoC 是什麼

IoC(Inversion of Control),即 「控制反轉」。在開發中, IoC 意味着你設計好的對象交給容器控制,而不是使用傳統的方式,在對象內部直接控制。  

如何理解好 IoC 呢?理解好 IoC 的關鍵是要明確 「誰控制誰,控制什麼,爲什麼是反轉,哪些方面反轉了」,咱們來深刻分析一下。  

  • 誰控制誰,控制什麼:在傳統的程序設計中,咱們直接在對象內部經過 new 的方式建立對象,是程序主動建立依賴對象; 而 IoC 是有專門一個容器來建立這些對象,即由 IoC 容器控制對象的建立

    誰控制誰?固然是 IoC 容器控制了對象;控制什麼?主要是控制外部資源(依賴對象)獲取。

  • 爲什麼是反轉了,哪些方面反轉了:有反轉就有正轉,傳統應用程序是由咱們本身在程序中主動控制去獲取依賴對象,也就是正轉; 而反轉則是由容器來幫忙建立及注入依賴對象

    爲什麼是反轉?由於由容器幫咱們查找及注入依賴對象,對象只是被動的接受依賴對象,因此是反轉了;哪些方面反轉了?依賴對象的獲取被反轉了。

3、IoC 能作什麼

IoC 不是一種技術,只是一種思想,是面向對象編程中的一種設計原則,能夠用來減低計算機代碼之間的耦合度。

傳統應用程序都是由咱們在類內部主動建立依賴對象,從而致使類與類之間高耦合,難於測試; 有了 IoC 容器後,把建立和查找依賴對象的控制權交給了容器,由容器注入組合對象,因此對象之間是鬆散耦合。 這樣也便於測試,利於功能複用,更重要的是使得程序的整個體系結構變得很是靈活。  

其實 IoC 對編程帶來的最大改變不是從代碼上,而是思想上,發生了 「主從換位」 的變化。應用程序原本是老大,要獲取什麼資源都是主動出擊,但在 IoC 思想中,應用程序就變成被動了,被動的等待 IoC 容器來建立並注入它所需的資源了。    

4、IoC 與 DI 之間的關係

對於控制反轉來講,其中最多見的方式叫作 依賴注入,簡稱爲 DI(Dependency Injection)。

組件之間的依賴關係由容器在運行期決定,形象的說,即由容器動態的將某個依賴關係注入到組件之中。 依賴注入的目的並不是爲軟件系統帶來更多功能,而是爲了提高組件重用的頻率,併爲系統搭建一個靈活、可擴展的平臺。

經過依賴注入機制,咱們只須要經過簡單的配置,而無需任何代碼就可指定目標須要的資源,完成自身的業務邏輯,而不須要關心具體的資源來自何處,由誰實現。

理解 DI 的關鍵是 「誰依賴了誰,爲何須要依賴,誰注入了誰,注入了什麼」: 

  • 誰依賴了誰:固然是應用程序依賴 IoC 容器;
  • 爲何須要依賴:應用程序須要 IoC 容器來提供對象須要的外部資源(包括對象、資源、常量數據);
  • 誰注入誰:很明顯是 IoC 容器注入應用程序依賴的對象;
  • 注入了什麼:注入某個對象所需的外部資源(包括對象、資源、常量數據)。

那麼 IoC 和 DI 有什麼關係?其實它們是同一個概念的不一樣角度描述,因爲控制反轉的概念比較含糊(可能只是理解爲容器控制對象這一個層面,很難讓人想到誰來維護依賴關係),因此 2004 年大師級人物 Martin Fowler 又給出了一個新的名字:「依賴注入」,相對 IoC 而言,「依賴注入」 明確描述了被注入對象依賴 IoC 容器配置依賴對象

總的來講, 控制反轉(Inversion of Control)是說建立對象的控制權發生轉移,之前建立對象的主動權和建立時機由應用程序把控,而如今這種權利轉交給 IoC 容器,它就是一個專門用來建立對象的工廠,你須要什麼對象,它就給你什麼對象。 有了 IoC 容器,依賴關係就改變了,原先的依賴關係就沒了,它們都依賴 IoC 容器了,經過 IoC 容器來創建它們之間的關係。 

前面介紹了那麼多的概念,如今咱們來看一下未使用依賴注入框架和使用依賴注入框架之間有什麼明顯的區別。

4.1 未使用依賴注入框架

假設咱們的服務 A 依賴於服務 B,即要使用服務 A 前,咱們須要先建立服務 B。具體的流程以下圖所示:

從上圖可知,未使用依賴注入框架時,服務的使用者須要關心服務自己和其依賴的對象是如何建立的,且須要手動維護依賴關係。若服務自己須要依賴多個對象,這樣就會增長使用難度和後期的維護成本。對於上述的問題,咱們能夠考慮引入依賴注入框架。下面咱們來看一下引入依賴注入框架,總體流程會發生什麼變化。

4.2 使用依賴注入框架

使用依賴注入框架以後,系統中的服務會統一註冊到 IoC 容器中,若是服務有依賴其餘服務時,也須要對依賴進行聲明。當用戶須要使用特定的服務時,IoC 容器會負責該服務及其依賴對象的建立與管理工做。具體的流程以下圖所示:

到這裏咱們已經介紹了 IoC 與 DI 的概念及特色,接下來咱們來介紹 DI 的應用。

5、DI 的應用

DI 在前端和服務端都有相應的應用,好比在前端領域的表明是 AngularJSAngular,而在服務端領域是 Node.js 生態中比較出名的 NestJS。接下來阿寶哥將簡單介紹一下 DI 在 AngularJS/Angular 和 NestJS 中的應用。

5.1 DI 在 AngularJS 中的應用

在 AngularJS 中,依賴注入是其核心的特性之一。在 AngularJS 中聲明依賴項有 3 種方式:

// 方式一: 使用 $inject annotation 方式
let fn = function (a, b) {};
fn.$inject = ['a', 'b'];

// 方式二: 使用 array-style annotations 方式
let fn = ['a', 'b', function (a, b) {}];

// 方式三: 使用隱式聲明方式 
let fn = function (a, b) {}; // 不推薦

對於以上的代碼,相信使用過 AngularJS 的小夥們都不會陌生。做爲 AngularJS 核心功能特性的 DI 仍是蠻強大的,但隨着 AngularJS 的普及和應用的複雜度不斷提升,AngularJS DI 系統的問題就暴露出來了。

這裏阿寶哥簡單介紹一下 AngularJS DI 系統存在的幾個問題:

  • 內部緩存: AngularJS 應用程序中全部的依賴項都是單例,咱們不能控制是否使用新的實例;
  • 命名空間衝突: 在系統中咱們使用字符串來標識服務的名稱,假設咱們在項目中已有一個 CarService,然而第三方庫中也引入了一樣的服務,這樣的話就容易出現混淆。

因爲 AngularJS DI 存在以上的問題,因此在後續的 Angular 從新設計了新的 DI 系統。

5.2 DI 在 Angular 中的應用

之前面汽車的例子爲例,咱們能夠把汽車、發動機、底盤和車身這些認爲是一種 「服務」,因此它們會以服務提供者的形式註冊到 DI 系統中。爲了能區分不一樣服務,咱們須要使用不一樣的令牌(Token)來標識它們。接着咱們會基於已註冊的服務提供者建立注入器對象。

以後,當咱們須要獲取指定服務時,咱們就能夠經過該服務對應的令牌,從注入器對象中獲取令牌對應的依賴對象。上述的流程的具體以下圖所示:

好的,瞭解完上述的流程。下面咱們來看一下如何使用 Angular 內置的 DI 系統來 「造車」。

5.2.1 car.ts
// car.ts
import { Injectable, ReflectiveInjector } from '@angular/core';

// 配置Provider
@Injectable({
  providedIn: 'root',
})
export class Body {}

@Injectable({
  providedIn: 'root',
})
export class Chassis {}

@Injectable({
  providedIn: 'root',
})
export class Engine {
  start() {
    console.log('引擎發動了');
  }
}

@Injectable()
export default class Car {
  // 使用構造注入方式注入依賴對象
  constructor(
    private engine: Engine,
    private body: Body,
    private chassis: Chassis
  ) {}

  run() {
    this.engine.start();
  }
}

const injector = ReflectiveInjector.resolveAndCreate([
  Car,
  Engine,
  Chassis,
  Body,
]);

const car = injector.get(Car);
car.run();

在以上代碼中咱們調用 ReflectiveInjector 對象的 resolveAndCreate 方法手動建立注入器,而後根據車輛對應的 Token 來獲取對應的依賴對象。經過觀察上述代碼,你能夠發現,咱們已經不須要手動地管理和維護依賴對象了,這些 「髒活」、「累活」 已經交給注入器來處理了。

此外,若是要能正常獲取汽車對象,咱們還須要在 app.module.ts 文件中聲明 Car 對應 Provider,具體以下所示:

5.2.2 app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import Car, { Body, Chassis, Engine } from './car';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [{ provide: Car, deps: [Engine, Body, Chassis] }],
  bootstrap: [AppComponent],
})
export class AppModule {}

5.3 DI 在 NestJS 中的應用

NestJS 是構建高效,可擴展的 Node.js Web 應用程序的框架。 它使用現代的 JavaScript 或 TypeScript(保留與純 JavaScript 的兼容性),並結合 OOP(面向對象編程),FP(函數式編程)和FRP(函數響應式編程)的元素。

在底層,Nest 使用了 Express,但也提供了與其餘各類庫的兼容,例如 Fastify,能夠方便地使用各類可用的第三方插件。

近幾年,因爲 Node.js,JavaScript 已經成爲 Web 前端和後端應用程序的「通用語言」,從而產生了像 AngularReactVue 等使人耳目一新的項目,這些項目提升了開發人員的生產力,使得能夠快速構建可測試的且可擴展的前端應用程序。 然而,在服務器端,雖然有不少優秀的庫、helper 和 Node 工具,可是它們都沒有有效地解決主要問題 —— 架構。

NestJS 旨在提供一個開箱即用的應用程序體系結構,容許輕鬆建立高度可測試,可擴展,鬆散耦合且易於維護的應用程序。 在 NestJS 中也爲咱們開發者提供了依賴注入的功能,這裏咱們以官網的示例來演示一下依賴注入的功能。

5.3.1 app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(): string {
    return 'Hello World!';
  }
}
5.3.2 app.controller.ts
import { Get, Controller, Render } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  @Render('index')
  render() {
    const message = this.appService.getHello();
    return { message };
  }
}

在 AppController 中,咱們經過構造注入的方式注入了 AppService 對象,當用戶訪問首頁的時候,咱們會調用 AppService 對象的 getHello 方法來獲取 'Hello World!' 消息,並把消息返回給用戶。固然爲了保證依賴注入能夠正常工做,咱們還須要在 AppModule 中聲明 providers 和 controllers,具體操做以下:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

其實 DI 並非 AngularJS/Angular 和 NestJS 所特有的,若是你想在其餘項目中使用 DI/IoC 的功能特性,阿寶哥推薦你使用 InversifyJS,它是一個可用於 JavaScript 和 Node.js 應用,功能強大、輕量的 IoC 容器。

InversifyJS 感興趣的小夥伴能夠自行了解一下,阿寶哥就不繼續展開介紹了。接下來,咱們將進入本文的重點,即介紹如何使用 TypeScript 實現一個簡單的 IoC 容器,該容器實現的功能以下圖所示:

6、手寫 IoC 容器

爲了讓你們能更好地理解 IoC 容器的實現代碼,阿寶哥來介紹一些相關的前置知識。

6.1 裝飾器

若是你有使用過 Angular 或 NestJS,相信你對如下的代碼不會陌生。

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient
  ) {}
}

在以上代碼中,咱們使用了 Injectable 裝飾器。該裝飾器用於表示此類能夠自動注入其依賴項。其中 @Injectable() 中的 @ 符號屬於語法糖。

裝飾器是一個包裝類,函數或方法併爲其添加行爲的函數。這對於定義與對象關聯的元數據頗有用。裝飾器有如下四種分類:

  • 類裝飾器(Class decorators)
  • 屬性裝飾器(Property decorators)
  • 方法裝飾器(Method decorators)
  • 參數裝飾器(Parameter decorators)

前面示例中使用的 @Injectable() 裝飾器,屬於類裝飾器。在該類裝飾器修飾的 HttpService 類中,咱們經過構造注入的方式注入了用於處理 HTTP 請求的 HttpClient 依賴對象。

6.2 反射

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient
  ) {}
}

以上代碼若設置編譯的目標爲 ES5,則會生成如下代碼:

// 忽略__decorate函數等代碼
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") 
      return Reflect.metadata(k, v);
};

var HttpService = /** @class */ (function () {
    function HttpService(httpClient) {
      this.httpClient = httpClient;
    }
    var _a;
    HttpService = __decorate([
        Injectable(),
        __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient)
           === "function" ? _a : Object])
    ], HttpService);
    return HttpService;
}());

經過觀察上述代碼,你會發現 HttpService 構造函數中 httpClient 參數的類型被擦除了,這是由於 JavaScript 是弱類型語言。那麼如何在運行時,保證注入正確類型的依賴對象呢?這裏 TypeScript 使用 reflect-metadata 這個第三方庫來存儲額外的類型信息。

reflect-metadata 這個庫提供了不少 API 用於操做元信息,這裏咱們只簡單介紹幾個經常使用的 API:

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// check for presence of a metadata key on the prototype chain of an object or property
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// get metadata value of a metadata key on the prototype chain of an object or property
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

// delete metadata from an object or property
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// apply metadata via a decorator to a constructor
@Reflect.metadata(metadataKey, metadataValue)
class C {
  // apply metadata via a decorator to a method (property)
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

對於上述的 API 只需簡單瞭解一下便可。在後續的內容中,咱們將介紹具體如何使用。這裏咱們須要注意如下兩個問題:

  • 對於類或函數,咱們須要使用裝飾器來修飾它們,這樣才能保存元數據。
  • 只有類、枚舉或原始數據類型能被記錄。接口和聯合類型做爲 「對象」 出現。這是由於這些類型在編譯後徹底消失,而類卻一直存在。

6.3 定義 Token 和 Provider

瞭解完裝飾器與反射相關的基礎知識,接下來咱們來開始實現 IoC 容器。咱們的 IoC 容器將使用兩個主要的概念:令牌(Token)和提供者(Provider)。令牌是 IoC 容器所要建立對象的標識符,而提供者用於描述如何建立這些對象。

IoC 容器最小的公共接口以下所示:

export class Container {
  addProvider<T>(provider: Provider<T>) {} // TODO
  inject<T>(type: Token<T>): T {} // TODO
}

接下來咱們先來定義 Token:

// type.ts
interface Type<T> extends Function {
  new (...args: any[]): T;
}

// provider.ts
class InjectionToken {
  constructor(public injectionIdentifier: string) {}
}

type Token<T> = Type<T> | InjectionToken;

Token 類型是一個聯合類型,既能夠是一個函數類型也能夠是 InjectionToken 類型。AngularJS 中使用字符串做爲 Token,在某些狀況下,可能會致使衝突。所以,爲了解決這個問題,咱們定義了 InjectionToken 類,來避免出現命名衝突問題。

定義完 Token 類型,接下來咱們來定義三種不一樣類型的 Provider:

  • ClassProvider:提供一個類,用於建立依賴對象;
  • ValueProvider:提供一個已存在的值,做爲依賴對象;
  • FactoryProvider:提供一個工廠方法,用於建立依賴對象。
// provider.ts
export type Factory<T> = () => T;

export interface BaseProvider<T> {
  provide: Token<T>;
}

export interface ClassProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useClass: Type<T>;
}

export interface ValueProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useValue: T;
}

export interface FactoryProvider<T> extends BaseProvider<T> {
  provide: Token<T>;
  useFactory: Factory<T>;
}

export type Provider<T> =
  | ClassProvider<T>
  | ValueProvider<T>
  | FactoryProvider<T>;

爲了更方便的區分這三種不一樣類型的 Provider,咱們自定義了三個類型守衛函數:

// provider.ts
export function isClassProvider<T>(
  provider: BaseProvider<T>
): provider is ClassProvider<T> {
  return (provider as any).useClass !== undefined;
}

export function isValueProvider<T>(
  provider: BaseProvider<T>
): provider is ValueProvider<T> {
  return (provider as any).useValue !== undefined;
}

export function isFactoryProvider<T>(
  provider: BaseProvider<T>
): provider is FactoryProvider<T> {
  return (provider as any).useFactory !== undefined;
}

6.4 定義裝飾器

在前面咱們已經提過了,對於類或函數,咱們須要使用裝飾器來修飾它們,這樣才能保存元數據。所以,接下來咱們來分別建立 InjectableInject 裝飾器。

6.4.1 Injectable 裝飾器

Injectable 裝飾器用於表示此類能夠自動注入其依賴項,該裝飾器屬於類裝飾器。在 TypeScript 中,類裝飾器的聲明以下:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) 
  => TFunction | void;

類裝飾器顧名思義,就是用來裝飾類的。它接收一個參數:target: TFunction,表示被裝飾的類。下面咱們來看一下 Injectable 裝飾器的具體實現:

// Injectable.ts
import { Type } from "./type";
import "reflect-metadata";

const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY");

export function Injectable() {
  return function(target: any) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    return target;
  };
}

在以上代碼中,當調用完 Injectable 函數以後,會返回一個新的函數。在新的函數中,咱們使用 reflect-metadata 這個庫提供的 defineMetadata API 來保存元信息,其中 defineMetadata API 的使用方式以下所示:

// define metadata on an object or property
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

Injectable 類裝飾器使用方式也簡單,只須要在被裝飾類的上方使用 @Injectable() 語法糖就能夠應用該裝飾器:

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient
  ) {}
}

在以上示例中,咱們注入的是 Type 類型的 HttpClient 對象。但在實際的項目中,每每會比較複雜。除了須要注入 Type 類型的依賴對象以外,咱們還可能會注入其餘類型的依賴對象,好比咱們但願在 HttpService 服務中注入遠程服務器的 API 地址。針對這種情形,咱們須要使用 Inject 裝飾器。

6.4.2 Inject 裝飾器

接下來咱們來建立 Inject 裝飾器,該裝飾器屬於參數裝飾器。在 TypeScript 中,參數裝飾器的聲明以下:

declare type ParameterDecorator = (target: Object, 
  propertyKey: string | symbol, parameterIndex: number ) => void

參數裝飾器顧名思義,是用來裝飾函數參數,它接收三個參數:

  • target: Object —— 被裝飾的類;
  • propertyKey: string | symbol —— 方法名;
  • parameterIndex: number —— 方法中參數的索引值。

下面咱們來看一下 Inject 裝飾器的具體實現:

// Inject.ts
import { Token } from './provider';
import 'reflect-metadata';

const INJECT_METADATA_KEY = Symbol('INJECT_KEY');

export function Inject(token: Token<any>) {
  return function(target: any, _: string | symbol, index: number) {
    Reflect.defineMetadata(INJECT_METADATA_KEY, token, target, `index-${index}`);
    return target;
  };
}

在以上代碼中,當調用完 Inject 函數以後,會返回一個新的函數。在新的函數中,咱們使用 reflect-metadata 這個庫提供的 defineMetadata API 來保存參數相關的元信息。這裏是保存 index 索引信息和 Token 信息。

定義完 Inject 裝飾器,咱們就能夠利用它來注入咱們前面所提到的遠程服務器的 API 地址,具體的使用方式以下:

const API_URL = new InjectionToken('apiUrl');

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}
}

6.5 實現 IoC 容器

目前爲止,咱們已經定義了 Token、Provider、Injectable 和 Inject 裝飾器。接下來咱們來實現前面所提到的 IoC 容器的 API:

export class Container {
  addProvider<T>(provider: Provider<T>) {} // TODO
  inject<T>(type: Token<T>): T {} // TODO
}
6.5.1 實現 addProvider 方法

addProvider() 方法的實現很簡單,咱們使用 Map 來存儲 Token 與 Provider 之間的關係:

export class Container {
  private providers = new Map<Token<any>, Provider<any>>();

  addProvider<T>(provider: Provider<T>) {
    this.assertInjectableIfClassProvider(provider);
    this.providers.set(provider.provide, provider);
  }
}

在 addProvider() 方法內部除了把 Token 與 Provider 的對應信息保存到 providers 對象中以外,咱們定義了一個 assertInjectableIfClassProvider 方法,用於確保添加的 ClassProvider 是可注入的。該方法的具體實現以下:

private assertInjectableIfClassProvider<T>(provider: Provider<T>) {
  if (isClassProvider(provider) && !isInjectable(provider.useClass)) {
    throw new Error(
        `Cannot provide ${this.getTokenName(
          provider.provide
     )} using class ${this.getTokenName(
          provider.useClass
     )}, ${this.getTokenName(provider.useClass)} isn't injectable`
   );
  }
}

在 assertInjectableIfClassProvider 方法體中,咱們使用了前面已經介紹的 isClassProvider 類型守衛函數來判斷是否爲 ClassProvider,若是是的話,會判斷該 ClassProvider 是否爲可注入的,具體使用的是 isInjectable 函數,該函數的定義以下:

export function isInjectable<T>(target: Type<T>) {
  return Reflect.getMetadata(INJECTABLE_METADATA_KEY, target) === true;
}

在 isInjectable 函數中,咱們使用 reflect-metadata 這個庫提供的 getMetadata API 來獲取保存在類中的元信息。爲了更好地理解以上代碼,咱們來回顧一下前面 Injectable 裝飾器:

const INJECTABLE_METADATA_KEY = Symbol("INJECTABLE_KEY");

export function Injectable() {
  return function(target: any) {
    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
    return target;
  };
}

若是添加的 Provider 是 ClassProvider,但 Provider 對應的類是不可注入的,則會拋出異常。爲了讓異常消息更加友好,也更加直觀。咱們定義了一個 getTokenName 方法來獲取 Token 對應的名稱:

private getTokenName<T>(token: Token<T>) {
  return token instanceof InjectionToken
    ? token.injectionIdentifier
    : token.name;
}

如今咱們已經實現了 Container 類的 addProvider 方法,這時咱們就可使用它來添加三種不一樣類型的 Provider:

const container = new Container();
const input = { x: 200 };

class BasicClass {}
// 註冊ClassProvider
container.addProvider({ provide: BasicClass, useClass:  BasicClass});
// 註冊ValueProvider
container.addProvider({ provide: BasicClass, useValue: input });
// 註冊FactoryProvider
container.addProvider({ provide: BasicClass, useFactory: () => input });

須要注意的是,以上示例中註冊三種不一樣類型的 Provider 使用的是同一個 Token 僅是爲了演示而已。下面咱們來實現 Container 類中核心的 inject 方法。

6.5.2 實現 inject 方法

在看 inject 方法的具體實現以前,咱們先來看一下該方法所實現的功能:

const container = new Container();
const input = { x: 200 };

container.addProvider({ provide: BasicClass, useValue: input });
const output = container.inject(BasicClass);
expect(input).toBe(output); // true

觀察以上的測試用例可知,Container 類中 inject 方法所實現的功能就是根據 Token 獲取與之對應的對象。在前面實現的 addProvider 方法中,咱們把 Token 和該 Token 對應的 Provider 保存在 providers Map 對象中。因此在 inject 方法中,咱們能夠先從 providers 對象中獲取該 Token 對應的 Provider 對象,而後在根據不一樣類型的 Provider 來獲取其對應的對象。

好的,下面咱們來看一下 inject 方法的具體實現:

inject<T>(type: Token<T>): T {
  let provider = this.providers.get(type);
  // 處理使用Injectable裝飾器修飾的類
  if (provider === undefined && !(type instanceof InjectionToken)) {
    provider = { provide: type, useClass: type };
    this.assertInjectableIfClassProvider(provider);
  }
  return this.injectWithProvider(type, provider);
}

在以上代碼中,除了處理正常的流程以外。咱們還處理一個特殊的場景,即沒有使用 addProvider 方法註冊 Provider,而是使用 Injectable 裝飾器來裝飾某個類。對於這個特殊場景,咱們會根據傳入的 type 參數來建立一個 provider 對象,而後進一步調用 injectWithProvider 方法來建立對象,該方法的具體實現以下:

private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {
  if (provider === undefined) {
    throw new Error(`No provider for type ${this.getTokenName(type)}`);
  }
  if (isClassProvider(provider)) {
    return this.injectClass(provider as ClassProvider<T>);
  } else if (isValueProvider(provider)) {
    return this.injectValue(provider as ValueProvider<T>);
  } else {
    return this.injectFactory(provider as FactoryProvider<T>);
  }
 }

injectWithProvider 方法內部,咱們會使用前面定義的用於區分三種不一樣類型 Provider 的類型守衛函數來處理不一樣的 Provider。這裏咱們先來看一下最簡單 ValueProvider,當發現注入的是 ValueProvider 類型時,則會調用 injectValue 方法來獲取其對應的對象:

// { provide: API_URL, useValue: 'https://www.semlinker.com/' }
private injectValue<T>(valueProvider: ValueProvider<T>): T {
  return valueProvider.useValue;
}

接着咱們來看如何處理 FactoryProvider 類型的 Provider,若是發現是 FactoryProvider 類型時,則會調用 injectFactory 方法來獲取其對應的對象,該方法的實現也很簡單:

// const input = { x: 200 };
// container.addProvider({ provide: BasicClass, useFactory: () => input });
private injectFactory<T>(valueProvider: FactoryProvider<T>): T {
  return valueProvider.useFactory();
}

最後咱們來分析一下如何處理 ClassProvider,對於 ClassProvider 類說,經過 Provider 對象的 useClass 屬性,咱們就能夠直接獲取到類對應的構造函數。最簡單的情形是該類沒有依賴其餘對象,但在大多數場景下,即將實例化的服務類是會依賴其餘的對象的。因此在實例化服務類前,咱們須要構造其依賴的對象。

那麼如今問題來了,怎麼獲取類所依賴的對象呢?咱們先來分析一下如下代碼:

const API_URL = new InjectionToken('apiUrl');

@Injectable()
export class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}
}

以上代碼若設置編譯的目標爲 ES5,則會生成如下代碼:

// 已省略__decorate函數的定義
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};

var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};

var HttpService = /** @class */ (function () {
    function HttpService(httpClient, apiUrl) {
        this.httpClient = httpClient;
        this.apiUrl = apiUrl;
    }
    var _a;
    HttpService = __decorate([
        Injectable(),
        __param(1, Inject(API_URL)),
        __metadata("design:paramtypes", [typeof (_a = typeof HttpClient !== "undefined" && HttpClient) 
          === "function" ? _a : Object, String])
    ], HttpService);
    return HttpService;
}());

觀察以上的代碼會不會以爲有點暈?不要着急,阿寶哥會逐一分析 HttpService 中的兩個參數。首先咱們先來分析 apiUrl 參數:

在圖中咱們能夠很清楚地看到,API_URL 對應的 Token 最終會經過 Reflect.defineMetadata API 進行保存,所使用的 Key 是 Symbol('INJECT_KEY')。而對於另外一個參數即 httpClient,它使用的 Key 是 "design:paramtypes",它用於修飾目標對象方法的參數類型。

除了 "design:paramtypes" 以外,還有其餘的 metadataKey,好比 design:typedesign:returntype,它們分別用於修飾目標對象的類型和修飾目標對象方法返回值的類型。

由上圖可知,HttpService 構造函數的參數類型最終會使用 Reflect.metadata API 進行存儲。瞭解完上述的知識,接下來咱們來定義一個 getInjectedParams 方法,用於獲取類構造函數中聲明的依賴對象,該方法的具體實現以下:

type InjectableParam = Type<any>;
const REFLECT_PARAMS = "design:paramtypes";

private getInjectedParams<T>(target: Type<T>) {
  // 獲取參數的類型
  const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (
      | InjectableParam
      | undefined
  )[];
  if (argTypes === undefined) {
      return [];
  }
  return argTypes.map((argType, index) => {
    // The reflect-metadata API fails on circular dependencies, and will return undefined
    // for the argument instead.
    if (argType === undefined) {
      throw new Error(
        `Injection error. Recursive dependency detected in constructor for type ${target.name} 
           with parameter at index ${index}`
      );
    }
    const overrideToken = getInjectionToken(target, index);
    const actualToken = overrideToken === undefined ? argType : overrideToken;
    let provider = this.providers.get(actualToken);
    return this.injectWithProvider(actualToken, provider);
  });
}

由於咱們的 Token 的類型是 Type<T> | InjectionToken 聯合類型,因此在 getInjectedParams 方法中咱們也要考慮 InjectionToken 的情形,所以咱們定義了一個 getInjectionToken 方法來獲取使用 @Inject 裝飾器註冊的 Token,該方法的實現很簡單:

export function getInjectionToken(target: any, index: number) {
  return Reflect.getMetadata(INJECT_METADATA_KEY, target, `index-${index}`) as Token<any> | undefined;
}

如今咱們已經能夠獲取類構造函數中所依賴的對象,基於前面定義的 getInjectedParams 方法,咱們就來定義一個 injectClass 方法,用來實例化 ClassProvider 所註冊的類。

// { provide: HttpClient, useClass: HttpClient }
private injectClass<T>(classProvider: ClassProvider<T>): T {
  const target = classProvider.useClass;
  const params = this.getInjectedParams(target);
  return Reflect.construct(target, params);
}

這時 IoC 容器中定義的兩個方法都已經實現了,咱們來看一下 IoC 容器的完整代碼:

// container.ts
type InjectableParam = Type<any>;

const REFLECT_PARAMS = "design:paramtypes";

export class Container {
  private providers = new Map<Token<any>, Provider<any>>();

  addProvider<T>(provider: Provider<T>) {
    this.assertInjectableIfClassProvider(provider);
    this.providers.set(provider.provide, provider);
  }

  inject<T>(type: Token<T>): T {
    let provider = this.providers.get(type);
    if (provider === undefined && !(type instanceof InjectionToken)) {
      provider = { provide: type, useClass: type };
      this.assertInjectableIfClassProvider(provider);
    }
    return this.injectWithProvider(type, provider);
  }

  private injectWithProvider<T>(type: Token<T>, provider?: Provider<T>): T {
    if (provider === undefined) {
      throw new Error(`No provider for type ${this.getTokenName(type)}`);
    }
    if (isClassProvider(provider)) {
      return this.injectClass(provider as ClassProvider<T>);
    } else if (isValueProvider(provider)) {
      return this.injectValue(provider as ValueProvider<T>);
    } else {
      // Factory provider by process of elimination
      return this.injectFactory(provider as FactoryProvider<T>);
    }
  }

  private assertInjectableIfClassProvider<T>(provider: Provider<T>) {
    if (isClassProvider(provider) && !isInjectable(provider.useClass)) {
      throw new Error(
        `Cannot provide ${this.getTokenName(
          provider.provide
        )} using class ${this.getTokenName(
          provider.useClass
        )}, ${this.getTokenName(provider.useClass)} isn't injectable`
      );
    }
  }

  private injectClass<T>(classProvider: ClassProvider<T>): T {
    const target = classProvider.useClass;
    const params = this.getInjectedParams(target);
    return Reflect.construct(target, params);
  }

  private injectValue<T>(valueProvider: ValueProvider<T>): T {
    return valueProvider.useValue;
  }

  private injectFactory<T>(valueProvider: FactoryProvider<T>): T {
    return valueProvider.useFactory();
  }

  private getInjectedParams<T>(target: Type<T>) {
    const argTypes = Reflect.getMetadata(REFLECT_PARAMS, target) as (
      | InjectableParam
      | undefined
    )[];
    if (argTypes === undefined) {
      return [];
    }
    return argTypes.map((argType, index) => {
      // The reflect-metadata API fails on circular dependencies, and will return undefined
      // for the argument instead.
      if (argType === undefined) {
        throw new Error(
          `Injection error. Recursive dependency detected in constructor for type ${target.name} 
             with parameter at index ${index}`
        );
      }
      const overrideToken = getInjectionToken(target, index);
      const actualToken = overrideToken === undefined ? argType : overrideToken;
      let provider = this.providers.get(actualToken);
      return this.injectWithProvider(actualToken, provider);
    });
  }

  private getTokenName<T>(token: Token<T>) {
    return token instanceof InjectionToken
      ? token.injectionIdentifier
      : token.name;
  }
}

最後咱們來簡單測試一下咱們前面開發的 IoC 容器,具體的測試代碼以下所示:

// container.test.ts
import { Container } from "./container";
import { Injectable } from "./injectable";
import { Inject } from "./inject";
import { InjectionToken } from "./provider";

const API_URL = new InjectionToken("apiUrl");

@Injectable()
class HttpClient {}

@Injectable()
class HttpService {
  constructor(
    private httpClient: HttpClient,
    @Inject(API_URL) private apiUrl: string
  ) {}
}

const container = new Container();

container.addProvider({
  provide: API_URL,
  useValue: "https://www.semlinker.com/",
});

container.addProvider({ provide: HttpClient, useClass: HttpClient });
container.addProvider({ provide: HttpService, useClass: HttpService });

const httpService = container.inject(HttpService);
console.dir(httpService);

以上代碼成功運行後,控制檯會輸出如下結果:

HttpService {
  httpClient: HttpClient {},
  apiUrl: 'https://www.semlinker.com/' }

很明顯該結果正是咱們所指望的,這表示咱們 IoC 容器已經能夠正常工做了。固然在實際項目中,一個成熟的 IoC 容器還要考慮不少東西,若是小夥伴想在項目中使用的話,阿寶哥建議能夠考慮使用 InversifyJS 這個庫。

若須要獲取完整 IoC 容器源碼的話,可在 全棧修仙之路 公衆號回覆 ioc 關鍵字,便可獲取。

7、參考資源

8、推薦閱讀

相關文章
相關標籤/搜索