了不得的 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、推荐阅读

相关文章
相关标签/搜索