【Nodejs】理解Nestjs的IoC与DI

什么是IOC

Interversion Of Control(控制反转)是面向对象编程中的一种设计原则:对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用注入给它。

设计IOC的目的:

  1. 把执行的任务和实现(implement)解耦。
  2. 使得开发者关注模块的任务设计上。
  3. 将模块从系统运行中抽离出来,取而代之的是契约关系。
  4. 防止更换模块产生副作用。
传统做法
使用IOC容器后

我们可以写一下伪代码,假设我们有一个Car类,需要让一辆车跑起来。

一般是这么写:

Class Wheel {
  constructor(size: number) {
    this.size = size
  }
}

class Car extends Wheel {
  constructor(size) {
    super(size)
  }
  run() {
    console.log(this.size);
  }
}

new Car(19).run();

很明显,Car 依赖了 Wheel 类,并且把 Wheel 和 Car 强耦合了在一起,在编写的时候更多关注与 Car 与 Wheel 类的整体系统。而忽略了拆分,在做单测的时候你不得不expect (new Car(4).size).toBe(4)这样去书写。

用IOC解耦之后:

Class Wheel {
  constructor(size) {
    this.size = size
  }
}

class Container {
  constructor() { this.modules = {} }
  provide(key, object) { this.modules[key] = object }
  get(key) { return this.modules[key] }
}

const carModule = new Container();

carModule.provide('wheel', new Wheel(4, carModule))

class Car {
  constructor(container: Container) {
    this.wheel = container.get('wheel');
  }
  run() {
    console.log(this.wheel)
  }
}

new Car().run();

这样子经过一个IOC容器进行梳理,从Wheel为Car的依赖子类,变成 Wheel 和 Car 同级,这就是控制反转。

Nestjs中的IOC

Nestjs当中的Module就是IOC的体现。

  • providers 依赖的service
  • imports 依赖别的模块的service
  • export 想要暴露的自身的service
  • controllers 模块路由

里面的providers就像我上述所说的IOC container。更具体的体现可以看module reference文档。

什么是DI

ioc是目的,di是手段。ioc是指让生成类的方式由传统方式(new)反过来,既程序员不调用new,需要类的时候由框架注入(di),是同一件不同层面的解读。

IOC 就是把传统的new SomeObject(props)操作,反转成用传入不同的对象为参数来实现。依赖注入DI是指程序运行过程中,若需要调用另一个对象协助时,无须在代码中创建被调用者,而是依赖于外部容器,由外部容器创建后传递给程序。

在Nest中我们往往能看到这样的代码

import { AppService } from './app.service'

// app.controllers.ts
@Controller()
class AppControler {
  constructor(appService: AppService) {}
  
  @Get()
  find(xx) {
    return this.appService.find(xx);
  }
}

AppController依赖了AppService,但是没有显性的在constructor中声明 this.appService = new AppService,而是交给了IOC 容器,由IOC容器去获取AppService,在调用的AppController的时候去帮我们自动注入,自动new AppService

接下来介绍一下如何实现一个依赖注入

Reflect Metadata

https://rbuckton.github.io/reflect-metadata/

https://www.typescriptlang.org/docs/handbook/decorators.html

基于Typescript实现一个DI(依赖注入)

在ts中使用Reflect Metadata,由于是实验性api需要下载reflect-metadata这个库。

npm i reflect-metadata --save

// ts.config
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true } }

在ts中使用这个库的时候,当我们在 constructor 注入依赖项时,我们可以在ReflectMetada中拿到该类型。

第一步:实现一个Car 依赖注入一个Framework对象

import 'reflect-metadata'

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

class Framework {
  public type = 'ev';
}

function Injectable() {
  return (target) => {
    console.log(target)
  }
}

// 
@Injectable()
class Car() {
  constructor(public framework: Framework) {
  }
}

function bootstrap<T>(target: Constructor<T>): T {
  // todo
  // assert car.framework.type to be ev
}

首先,在ts中,使用装饰器,它会在编译阶段自动的给我们注入metadata。上述代码编译过后是这样子的:

require("reflect-metadata");
class Framework {
    constructor() {
        this.type = 'ev';
    }
}
function Injectable() {
    return (target) => {
        console.log(target);
    };
}
let Car = class Car {
    constructor(framework) {
        this.framework = framework;
    }
};
Car = __decorate([
    Injectable(),
    __metadata("design:paramtypes", [Framework])
], Car);

我们可以看到,这里在class constructor中给我们自动注入了Framework对象。我们可以很轻易的拿到

Reflect.getMetadata('design:paramtypes', Car) // 输出 [Framework]

沿着这个思路,我们只要调用new方法时,把constructor拿出所有依赖注入的对象,帮他new方法注入。

所以boostrap可以这么写:

function bootstrap<T>(target: Constructor<T>): T {
  const deps = Reflect.getMetadata('design:paramtypes', target);
  return new target(...deps.map((ctor: Constructor<unknown>) => {
    return new ctor();
  }))
}

const car = bootstrap<Car>(Car)

结果:console.log(car.framework.type === 'ev') // true

第二步,如果依赖的对象也有依赖的对象,那么需要不断递归去bootstrap依赖的对象

在上述中,我们有了一个car对象,里面有framework属性,假设car有19寸轮毂,则framework里面会依赖一个Wheel,那么上述的bootstrap就会有问题了:

import 'reflect-metadata'

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

function Injectable() {
  return (target) => {
  }
}

@Injectable()
class Wheel {
  public size = 19;
}


@Injectable()
class Framework {
  public type = 'ev';
  constructor(public wheel: Wheel) {
  }
  
  run() {
    console.log(this.wheel.size);
  }
}

//
@Injectable()
class Car {
  constructor(public framework: Framework) {
  }
}

// todo bootstrap

这里,我们可以使用不断递归循环的方式去获得,但是最好的方法是做一层containerIOC容器去管理。这样,可以有效的知道所有依赖项,可以对依赖项进行管理。

class Injector extends Map {
  resolve<T>(target: Constructor<T>) {
    const deps = Reflect.getMetadata('design:paramtypes', target) || [];
    const depsInstance = deps.map((ctor: Constructor<unknown>) => {
      return this.resolve(ctor);
    })

    const classInstance = this.get(target);
    if (classInstance) {
      return classInstance;
    }

    const newClassInstance = new target(...depsInstance);
    
    this.set(target, newClassInstance)
    
    return newClassInstance;
  }
}


function bootstrap<T>(target: Constructor<T>): T {
  const container = new Injector();
  return container.resolve(target);
}

const car = bootstrap<Car>(Car)
console.log(car.framework.wheel.size === 19) // true

上述的Injector,我们还可以release所有依赖:

injector.clear() // 这样就可以把容器所有依赖给移除掉。

reference

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):