控制反转与依赖注入-NEST实现方法

控制反转

了解控制反转之前,要先了解为什么会有这个东西存在。

平时工作中,一定会碰到某些大坑,本来就是一个小小的需求,但是随着产品逻辑越加越多,里面会慢慢加入很多权限控制,逻辑判断,甚至插入一个新的需求,慢慢的一个小项目可能会积累出上千行代码,如何维护这些新旧逻辑一直是前端比较头大的事情。

image.png

这里随意举一个例子,比如我现在有一个很简单业务逻辑:小明要开着奥拓回家

本身就一个类,把需求写上去完事了,但是实际上如果不考虑扩展就会变得很糟糕

image.png
假设我们这个需求中,人、车、地点都是可以扩展的,并且里面有大量约束逻辑,比如这个车有几个轮子,能不能开起来等等,当业务扩展到一定程度之后,逻辑就需要遵循单一职责原则(SRP)

一个类,最好只负责一件事,只有一个引起它变化的原因。

通过对每个类的职责划分,就能很轻松的把一个复杂逻辑拆分成几个互不相关的任务。代码可以大致如下

const target = new Go();
    class Go {
      constructor() {
        this.person = new Person('小明');
        this.car = new Car('奥拓');
        this.destination = new Destination();
        this.init()
      }




      init() {
        console.log(`${this.person.name()}:${this.car.drive(),this.car.name()},${this.destination.goHome()}`)
      }
    }

其实大部分需求都不会过于庞大,平时的业务用单一职责设计模式就可以解决,但是如果在复杂一些,比如:小明要开着三轮拿着扳手坨着老王拽着老李去医院上厕所,我们的需求类中就会出现大量的 new 来获取各种任务的输出。

类似这样:

    const a = new A();
    const b = new B('a');
    const c = new C('b');
    const d = new D();
    const e = new E();
    // ....

这个情况在设计组件或者框架时非常常见,如果我们想改其中一个类的输入 or 输出,可能会对后续相当多的任务造成影响,我们需要一个一个往下查,但是这样耦合度就非常高,可能某个对象的一点小修改就会引起连锁反应,全部的逻辑校验都要在需求类中进行修改,慢慢的这个需求类就会积累起想到多的逻辑判断,难以维护和调试。这时就可以使用 “控制反转”。

把自己程序一部分的执行控制交给某个第三方。即程序的调用方式,时间,次数等,不由定义方“控制”。

即依赖的哪个具体的实例对象被调用,不由调用方“控制”了。

如果依赖对象的获取逻辑被反转,具体生成什么依赖对象和什么时候生成都由对象之外某个容器来决定。对象只要在用到依赖对象的时候能获取到就可以了。这就是控制反转。

这里我们把原来的需求类变成了一个工厂类,工厂场类只是做业务聚合,甚至工厂类的产出都可以不放在业务中,由外部控制,如下↓

    class Go {
        constructor(options) {
          this.person = options.person;
          this.car = options.car;
          this.destination = options.destination;
          this.options = options
          this.init();
        }
        init() {
          this.options.gogogo(this)
        }
    }



    new Go({
        person: new Person('小明'),
        car: new Car('奥拓'),
        destination: new Destination('回家'),
      go(thiz) {
        console.log(thiz.person.target, thiz.car.target, thiz.destination.target)
    })

这样就是一个简单的控制反转,可以看到依赖是由外部往工厂类中“注入”,采用依赖注入技术之后,工厂类中至于要负责定义私有对象,不需要 new 这些类型,具体的 new 逻辑交给外部配置,工厂不用关心你到底是 new 了一个什么,只要给我我需要的方法即可。

但是如上的代码在实际需求中会有许多问题存在:

  1. 如果各个外部模块还存在互相依赖怎么办?
  2. 如果新增了一个外部类怎么办?

假设需要注入的对象有互相之间的逻辑依赖,那这种外部注入的模式去哪里拿到他需要的上级类?而如果我们这个需求做完之后产品还是框框往里面加逻辑,那工厂中定义的私有对象是不是也会越来越多?

为了解决这个问题,我们需要优化依赖注入的逻辑,依赖注入不应该直接塞到工厂中就完事了,要不干脆直接写一个工厂模式收尾就可以了。

我们需要提前准备一个执行队列,将逻辑类提前放入队列中,等到需要他们执行的时候,从队列中挨个取出来。

同时在 new 之前,我们把工厂函数的 this 传入逻辑类中,逻辑类需要的前置参数也会被提前放到 this 中,这样就解决了我们第一个问题。

至于第二个问题就更好解决了,我们可以再执行队列初始化逻辑类的时候,把逻辑类的实例当做返回值存入工厂中即可。

简单来说就是把高层模块所依赖的模块通过传参的方式「注入」到模块内部,上面的代码可以通过依赖注入的方式改造成如下方式:

class Person {
  constructor(name) {
    this.name = name;
  }
}
class Car {
  constructor(car) {
    this.car = car;
  }
}
class Destination {
  constructor(destination) {
    this.destination = destination;
  }
}

const _Person = {
  init(_thiz) {
    const target = new Person(_thiz.options.name);
    return target.name;
  },
};

const _Car = {
  init(_thiz) {
    target = new Car(_thiz.options.car);
    _thiz.carType = "我一定是四驱的";
    return target.car;
  },
};
const _Destination = {
  init(_thiz) {
    target = new Destination(_thiz.options.destination);
    return target.destination;
  },
};

const moduleFactory = [];
class Go {
  constructor(options) {
    this.options = options;
    this.init();
  }
  static inject(args) {
    args.map((item) => {
      return moduleFactory.push(item);
    });
  }
  init() {
    const _return = moduleFactory.map((item) => {
      return item.init(this);
    });
  }
}


Go.inject([_Person, _Car, _Destination]);

new Go({
  name: "小明",
  car: "奥拓",
  destination: "回家",
});

这里的 moduleFactory 负责维护业务逻辑所需要的逻辑类,需要的时候直接从其中提取并初始化即可。
每一个逻辑类都会被额外包括一层负责接受工厂的 this,并且在这里做最后的修改。

如何用 TS + decorator 实现 DI

上面的工厂是之前常用的方法,但是实际需求中我们有一种更优秀的手段来使用 DI,也是常见的框架 DI 的标准做法。就是通过 Reflect Metadata 把逻辑类当成一个元数据提前收集起来,需要使用时直接从元数据中提取即可。

image.png

这块可以慢慢往后看,先说一些前置条件:

Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。TypeScript 在 1.5+ 的版本已经支持(reflect-metadata)

这里就是通过 Reflect.defineMetadata("class_reflect:param", "2", A);class A 中存入一个元数据,元数据的 key value 分别是 class_reflect:param 2。

class A {
  a: string;
  constructor() {
    this.a = "1";
  }
}

Reflect.defineMetadata("class_reflect:param", "2", A);
console.log(Reflect.getMetadata("class_reflect:param", A));

另外 Reflect.metadata 还当作装饰器使用,当修饰类时,在类上添加元数据,当修饰类属性时,在类原型的属性上添加元数据,如:

@Reflect.metadata('inClass', 'A')
class Test {

  @Reflect.metadata('inMethod', 'B')
  public hello(): string {

    return 'hello world';
  }

}


console.log(Reflect.getMetadata('inClass', Test)); // 'A'
console.log(Reflect.getMetadata('inMethod', new Test(), 'hello')); // 'B'

Reflect.metadata 在实际使用中一般会放在装饰器类中使用,在业务逻辑进行中对元数据进行标记,并在合适时机获取他的值。

function classDecorator(): ClassDecorator {
  return target => {
    // 在类上定义元数据,key 为 `classMetaData`,value 为 `a`
    Reflect.defineMetadata('classMetaData', 'a', target);
  };
}


function methodDecorator(): MethodDecorator {
  return (target, key, descriptor) => {
    // 在类的原型属性 'someMethod' 上定义元数据,key 为 `methodMetaData`,value 为 `b`
    Reflect.defineMetadata('methodMetaData', 'b', target, key);
  };
}

@classDecorator()
class SomeClass {
  @methodDecorator()
  someMethod() {}
}

Reflect.getMetadata('classMetaData', SomeClass); // 'a'
Reflect.getMetadata('methodMetaData', new SomeClass(), 'someMethod'); // 'b'

了解了 Reflect.metadata 的使用,再说说他实际的作用

Angular 中的依赖注入

直接先来一段经典代码

import "reflect-metadata";

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


const Injectable = (): ClassDecorator => (target) => {};


class OtherService {
  a = 1;
}




@Injectable()
class TestService {
  constructor(public readonly otherService: OtherService) {}



  testMethod() {
    console.log(this.otherService.a);
  }

}


const Factory = <T>(target: Constructor<T>): T => {
  // 获取所有注入的服务
  const providers = Reflect.getMetadata("design:paramtypes", target); // [OtherService]
  const args = providers.map((provider: Constructor) => new provider());
  return new target(...args);
};

Factory(TestService).testMethod(); // 1

在这个例子中,无需手动注入的关键就在于这个工厂函数,工厂函数中通过 Reflect.getMetadata("design:paramtypes", 目标类); 方法获取目标类中 design:paramtypes 的数据。这个数据中有当前类所依赖的目标类数据,接下来说说 design:paramtypes 的值是从哪里被赋值的。

design:paramtypes 来自于 tsc 编译后生成的文件,我们需要在 tsconfig.json 中修改配置文件

  • "experimentalDecorators": true,
  • "emitDecoratorMetadata": true

也就是说,经过 tsc 编译后的 ts 文件,如果存在装饰器,在编译后的文件中会被自动注入一些元数据,元数据中包含构造器中所使用的类型定义。

本例的打包文件删减后如下:

"use strict";
var __decorate = function (decorators, target, key, desc) {
  reflect_target = Reflect.decorate(decorators, target, key, desc);
  return reflect_target;
};
var __metadata = function (k, v) {
  return Reflect.metadata(k, v);
};

var Injectable = function () {
  return function (target) {};
};
var OtherService = (function () {
  // ......
})();
var TestService = (function () {
  // ......
  TestService = __decorate(
    [Injectable(), __metadata("design:paramtypes", [OtherService])],
    TestService
  );
  return TestService;
})();

这里的 __metadata 就是 tsc 在编译后自动给 TestService 注入 design:paramtypes 元数据,OtherService 是构造函数中使用的目标类(定义的时候要注入 OtherService 的类型,如果设定类型是 any,则编译后这里就是 Object,不能满足需求)。所以我们才能从工厂函数中提取 design:paramtypes 来确定他需要的入参。

在 tsc 中对装饰器注入的元数据有很多,比如下面的例子中,经过 tsc 编译后我们可以通过 metadata 获取到当前方法中的入参,出参,类型

@Reflect.metadata("inClass", "A")
class Test {

  @Reflect.metadata("inMethod", "B")
  public hello(): string {

    return "hello world";
  }

}

// 输出
 __decorate([
   Reflect.metadata("inMethod", "B"),
   __metadata("design:type", Function),
   __metadata("design:paramtypes", []),
   __metadata("design:returntype", String)
 ], Test.prototype, "hello", null);
Test = __decorate([
  Reflect.metadata("inClass", "A")
], Test);

这样我们在 Factory 中手动将元数据中的使用类型提取出来,然后挨个 new 出来就好了,这样我们就不用再重复设置各种私有函数,新的类型加入的时候我们直接增加构造器中的引入即可。

这种 IOC 的更进一步应用就是 Nest。

Nest

Nest 是一个用于构建高效、可扩展的 Node.js Web 应用程序的框架。提供组织代码的架构能力

Nest 可以简单理解成一个基于 Express 的 Node 框架,我们可以通过 Nest 做后端数据处理并且与数据库交互做增删改查。

Nest 本身的设计理念和 Angular 相似,有和前端类似的 MVC 框架,并且利用了 IOC、AOP 等设计模式。基本可以理解是比 express更上层的企业级node框架

Nest 实现思路

Nest 借鉴了 Angular 的思路,通过 ts + decorator 来实现 DI,但是 Nest 作为一个后端框架来说,他需要承载分非常多的接口拦截和业务逻辑, 他们要在 Nest 中互相绑定,还需要在最后对数据库中的数据做增删改查,这就导致了创建这些对象是很复杂的,每一条业务线都需要一连串的类绑定到一起互相干扰,为了解决这中庞杂的逻辑关系, Nest 在确定了 IOC 的基础思路之后,结合了后端常用的 MVC 架构,做了一个简单的三层架构。Module,Controller,Service 三层。

请求会先发送给 Controller,由它调度 Model 层的 Service 来完成业务逻辑,然后返回对应的 View。其中 Controller 负责拦截接口请求,Service 负责返回具体的业务逻辑输出。

我们会在 Model 加载大量的 Controller 和需要使用到的 Service 逻辑层。然后 Nest 会在工厂中将当前使用到的 Controller 和 Service 统统存放在内存中,当 Controller 中构造器需要使用什么 Service,工厂会自动 new 出一个 Service 实例出来给 Controller 使用,这里面就是 IOC 的进一步拓展。

这里面 providers 就是可以提供给 Controllers 的所有业务逻辑模块。

这样就做到了接口处理(controller)和业务逻辑(service)的解耦,业务逻辑不用关心请求、鉴权、数据转换等各种问题,专心干自己的就可以了,十分方便扩展。也就是后端的模块化。

image.png

接下来我们简单实现一下 Nest 中 Module 使用的控制器。

实现 Nest 中的 IOC

首先我们要先实现 Nest 中的工厂,工厂中可以自动对 Controller 和 Provider 中需要使用的类实例化。这里就用到了上述的 metadata 方式。

在 Model 中将所涉及的 Controller 和 Service 都放在 @Model 装饰器中等待引入,在 Controller 的构造函数中标注当前 Controller 需要用到的 Service 逻辑层。

  1. Model 装饰器中设置 controller、provider 两个参数,分别存入需要用到的 Controller 和 Service,Model 装饰器会将 controller,provider 以 key,value 的形式分别存入元数据中。
  2. 在 Factory 工厂中初始化一个容器,这个容器负责维护一个 Map,这个 Map 会存入当前 Model 涉及到的所有 Service,当有 Controller 需要提取出 Service 时,我们都从这个容器中获取。
  3. 然后对每一个使用到的 Controller ,通过 "design:paramtypes" 获取到他构造函数中需求的逻辑类,把 Service 挨个实例化就 ok 了。
import "reflect-metadata";

interface Type<T> {
  new (...args: any[]): T;
}


interface ClassProvider<T> {
  provide: Type<T>;
  useClass: Type<T>;
}

class Container {
  providers = new Map<Type<any>, ClassProvider<any>>();



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


  inject(token: Type<any>) {
    return this.providers.get(token)?.useClass;
  }
}
const Controller = (): ClassDecorator => (target) => {
  // 各种 Reflect.metadata 注入
  return target;
};
const Inject = (): ClassDecorator => (target) => {
  // 各种 Reflect.metadata 注入
  return target;
};

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

function Module(metadata: any) {
  const propsKeys = Object.keys(metadata);
  return (target: any) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        Reflect.defineMetadata(property, metadata[property], target);
      }
    }
  };
}

function Factory<T>(target: Constructor<T>): any {
  const container = new Container();
  // 1. 先把 provider 全部都放到容器里
  const providers = Reflect.getMetadata("providers", target);
  for (let i = 0; i < providers.length; i++) {
    container.addProvider({ provide: providers[i], useClass: providers[i] });
  }


  // 2. 获取模块中的所有 controller
  const controllers = Reflect.getMetadata("controllers", target);
  for (let i = 0; i < controllers.length; i++) {
    // 3. 提取当前 controller 中所有的 provider id
    const currentDeps = Reflect.getMetadata(
      "design:paramtypes",
      controllers[i]
    );

    // 4. 通过 provider id 获取当前 controller 中所有的 provider
    const depsInstance = currentDeps.map((provider: Type<any>) => {
      return new (container.inject(provider) as Constructor)();
    });
    // 5. 实例化 controller
    const controllerInstance = new controllers[i](...depsInstance);
    console.log(controllerInstance.getService());
  }
  return container;
}

// =========================
@Inject()
class AppService {
  check() {
    return "this is AppService";
  }
}

@Inject()
class AppService2 {
  check() {
    return "this is AppService2";
  }
}

@Controller()
class AppController {
  constructor(public appService: AppService) {}
  public getService() {
    return this.appService.check();
  }
}

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

Factory(AppModule);

const providers = Reflect.getMetadata("providers", AppModule);
const controllers = Reflect.getMetadata("controllers", AppModule);
const personMeta = Reflect.getMetadata("design:paramtypes", AppController);
console.log(providers, "---providers---");
console.log(controllers, "---controllers---");
console.log(personMeta, "---personMeta---");

实现 Nest 中的请求拦截装饰器

Nest 作为 Node 框架还需要做数据请求传输,Nest 中数据请求是用 express 或者 fastify 处理的,这里使用 express 模拟一下内部实现。

我们的逻辑是要把请求路径、方式也通过装饰器传入到 Controller 中,这样可以将请求和 Controller 挂钩, Controller 负责拦截配置请求的请求头、请求体、响应头等。

其中路径、方式也可以通过元数据存储,这样做的目的是在 Factory 中将请求的信息和处理逻辑关联起来,一起交给 Express 做绑定。

这里举个例子:

  1. 先通过 @Get 装饰器将请求路径和方法存入 metadata 中
  2. 通过 mapRoute 函数将处理方法和请求信息提取出来输出
const METHOD_METADATA = "method";
const PATH_METADATA = "path";


export const Controller = (path: string): ClassDecorator => {
  return (target) => {
    Reflect.defineMetadata(PATH_METADATA, path, target);
  };
};





const createMappingDecorator =
  (method: string) =>
  (path: string): MethodDecorator => {
    return (target, key, descriptor: any) => {
      Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
      Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value);
    };
  };


export const Get = createMappingDecorator("get");
export const Post = createMappingDecorator("post");

export function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);

  // 筛选出类的 methodName
  const methodsNames = Object.getOwnPropertyNames(prototype).filter((item) => {
    return !isConstructor(item) && isFunction(prototype[item]);
  });
  return methodsNames.map((methodName) => {
    const fn = prototype[methodName];

    // 取出定义的 metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    const method = Reflect.getMetadata(METHOD_METADATA, fn);
 /**
 * [{
 *    route: '/a',
 *    method: 'GET',
 *    fn: getService() { ... },
 *    methodName: 'getService'
 *  }]
 */
    return {
      route,
      method,
      fn,
      methodName,
    };
  });
}


function isConstructor(config: string) {
  return config == "constructor";
}

function isFunction(config: any) {
  return config !== "constructor";
}

我们可以在 Factory 实例化之后,将实例化的 Controller 传入到 mapRoute 中,最后输出一个关联列表对象,将这个对象直接塞到 Express 监听端口即可。

import * as express from "express";
import { AppModule, Container } from "./03-nest-03";
import { mapRoute } from "./express-decorator";

Factory(AppModule) as any;

async function Factory<T>(target: Constructor<T>) {
  const app = express() as any;




  // ....
  for (let i = 0; i < controllers.length; i++) {
    //....
    // 5. 实例化 controller
    const controllerInstance = new controllers[i](...depsInstance);

    // 6. 传参到 express
    const expressConfig = mapRoute(controllerInstance);
    expressConfig.forEach((item) => {
      const { route, method, fn } = item;
      // 绑定到服务上,这里就是 mapRoute 传回的结构体
      app[method](route, fn.bind(controllerInstance));
    });
  }
  app.listen(3000);
}

Nest 中的 AOP

反正都说到 Nest 了,就顺便了解一下一个完整的后端逻辑还需要做什么

一个完整的后端逻辑,除了数据请求的正常处理之外,我们还需要在其中每一个关键步骤加上各种各样的日志、异常处理、权限控制等等,这里 Nest 使用的是类似 Express 洋葱模型的面向切面编程,不会将各种乱七八糟的中间处理放到逻辑处理中,而是在每一个关键步骤之间做一个无感知的切面。

AOP 的好处是可以把一些通用逻辑分离到切面中,保持业务逻辑的纯粹性,这样切面逻辑可以复用,还可以动态的增删。实现更进一步的业务解耦。

image.png

这样其实 Nest 部分的框架逻辑就已经理清了,当然实际应用中,我们还需要关注数据处理之前的负载均衡,常见的就是 LVS + Nginx 做 TCP 和 HTTP 的负载均衡,而在一个完整的分布式系统中,我们也需要加入 Zookeeper 相关的消息队列。为了运维方便,还需要加入 Docker 等来实现多个机器的同步部署。

前端大概要了解的就这么多了,总之 Nest 对于一个小前端来了解后端架构关系来说,还是一个比较容易入手学习的框架,如果小公司中有一些需求缺少后端,也可以直接先用 Nest 把后端逻辑层顶起来。

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYb4zhBI' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片