美文网首页
【Nodejs】理解Nestjs的IoC与DI

【Nodejs】理解Nestjs的IoC与DI

作者: 枸杞辣条 | 来源:发表于2021-03-27 18:41 被阅读0次

    什么是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

    相关文章

      网友评论

          本文标题:【Nodejs】理解Nestjs的IoC与DI

          本文链接:https://www.haomeiwen.com/subject/tqbhqltx.html