【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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容