Typescript学习(十九)控制反转与依赖注入

154 阅读10分钟

前面我们已经了解了Typescript中的装饰器, 我们知道了装饰器可以帮我们完成一些简单的操作, 但是, 这显然只是装饰器的一部分能力, 如果仅有这部分能力, 它根本不值得我们去学习; 所以, 本节我们要学习的一个很重要的概念: 控制反转和依赖注入

控制反转概念

控制反转, 英文名Inversion of Control, 即IOC何为控制反转? 先看看以下案例

class Feul {}

class Car {
  feul: Feul | undefined;
  run() {
    if (this.feul) {
      console.log('success! I can run!');
    } else {
      console.log('sorry, I can not run')
    }
  }
}
const car = new Car()
car.run() // sorry, I can not run

从以上案例可以知道, Car(汽车), 依赖于feul(汽油), 车有油了才能跑, 不给Car的feul属性赋值, 'Car'是run不起来的, 此时 怎么办? 简单, 我们可以给car'加油'

car.feul = new Feul('95#') // 给加95

但是如果Feul也有依赖呢? 比如, 必须依赖于有加油站, 或者说咱手头要有钱, 那怎么办? 我们就继续给Feul设置依赖条件?

Feul.isHasMoney = true
Feul.isHasStation = true
// ....也许还有

那如果依赖继续增加呢? 想要加油还必须有身份证, 那怎么办? 或者连我们的钱都规定了必须是现金! 那我们一个个加? 如果依赖项非常多, 层级非常深, 这显然是无法维护的; 所以, 我们必须要把这个反复设置依赖项的控制权交给程序, 让它来完成, 这种就叫控制反转, 也可以说是, 我们把控制权, 说白了就是干累活的'权利', 交给程序, 而不是我们自己手动来一个个抠! 所以, 我们希望能有一个容器(Container)帮我们处理好所有的依赖项和依赖关系:

const car = container.get(Car)
car.run() // 'success! I can run!'

控制反转终究只是一种设计原则, 即指导思想, 真正去实现, 还要依靠依赖查找和依赖注入;

依赖查找

关于依赖查找, 其实就是将依赖项的查找和实例化、赋值, 都放进一个类的方法中去执行, 通常需要传入一个key作为查找的依据

class Factory {
  static create(key) {
    // todo
  }
}

class Car {
  feul: Feul | undefined;
  run() {
    if (this.feul) {
      console.log('I have feul, so I can run');
    }
  }
  constructor() {
    this.feul = Factory.create('key');
  }
}

这样, 只要我们new Car(), feul属性就会被自动赋值

依赖注入

前面的依赖查找, 我们需要在每个类上增加一个额外的方法, 虽然也没多少代码, 但是偷懒是程序员的追求之一; 而依赖注入则更加简洁, 使用依赖注入, 我们只需要将各种依赖关系用装饰器标注清楚就行了, 而这正是装饰器的另一种重要用途;

@Provide()
class Car {
  @Inject()
  feul: Feul | undefined;
}

Provide装饰器负责将Car注册到容器中, Inject则是表示Car依赖于feul属性的值; 通过这种方式, 就能够确定依赖关系了, 之前说过, 元数据是描述数据的数据, 同样, 它也能描述一个属性需要的依赖项, 等到要执行的时候, 直接读取其元数据, 就能知道它的依赖关系了; 那么如何实现呢? 如何将描述好的关系, 转为实际的逻辑呢? 我们需要一个IOC容器

案例1, IOC容器

前面已经说了, 我们想要的效果就是, 写几个装饰器确定下依赖关系, 然后丢进一个容器就完事了:

@Provide()
class Car {
  @Inject()
  feul: Feul | undefined;
}
const car = container.get(Car)
car.run() // 'success! I can run!'

那么怎么实现这个控制反转(IOC)容器? 我们来整理下思路:

  1. 我们要有一个映射表, 里面存着所有注册了的类, 这个任务交给provide装饰器.
  2. 我们要将这个类所依赖的属性的类型也记录下来, 也存进一个映射表, 这个任务自然是交给Inject, 不过这个表要稍微复杂些, 里面必须包含这个属性属于哪个类, 以及这个属性需要的值是什么类型等信息
  3. 以上两个表建立起来后, 我们还要在get的时候, 查到注册表中的类(provide建立的表), 然后通过关系表(inject建立的表), 找出这个类所以依赖的属性以及它所能接受的类; 再从provide的映射表中找出这个类, 将其实例化后赋给这个属性!

好了, 按照这套逻辑, 我们来写代码, 首先, 我们的provide要负责建立映射表, 既然是映射表, 那肯定需要有键值, 值自然是其所修饰的类, 键呢? 可以是这个类的name, 也可以是用户传给provide的一个自定义键, 一个字符串

function Provide(key?: string): ClassDecorator {
  return function (target: any) {
    Container.set(key ?? target.name, target);
    Container.set(target, target);
  };
}

这里为什么要set一个target呢? 即 以整个类为一个键, 别忘了, 我们前面写的期望代码是这样的

const car = container.get(Car)

就是我哪怕传一个类作为键, 它也能从映射表中找到对应的类, 这是最简便的; 那如果我们不传类, 我们就想自己定义这个映射表的键名, 那就可以传一个字符串;

@Provide('myCar')
class Car {
 // ...
}
const car = Container.get('myCar');

所以我们又考虑了provide参数key存在的情况, 如果key不存在(undefined || null), 我们就取类的name; 这样写是为了兼容更多写法; 此时我们的映射表就有这些情况

Car -> Car

Car.name -> Car
// or
'myCar' -> Car

我们再来看看inject, 前面说了, inject必须负责建立关系映射表

function Inject(key?: string): PropertyDecorator {
  return function (prototype: any, propName: string | symbol) {
    Container.regsiterProperty(
      `${prototype.constructor.name}:${String(propName)}`,
      key ?? Reflect.getMetadata('design:type', prototype, propName)
    );
  };
}

注意了, 'design:type'为内置元数据, 可以获取其修饰的属性, 所需要的类, 我们可以看到, 关系映射表结构是这样的:

className:propName -> 该属性需要接受的类
className:propName -> 自定义的键

也就是, 类:类的属性 -> 属性需要的类 或者 自定义的键; 这里要注意的是这个映射表的值, 指的就是上一个映射表中的键! 即 我们通过类和属性这两个维度, 确定上一个映射表的键, 从而找到这个属性需要的那个类, 然后, 我们再实例化这个类, 将实例赋值给这个属性;

好了, 讲完了这两个装饰器, 我们要来看看Container里的逻辑该怎么实现了, 前面的代码中, 我们发现Container有set方法、regsiterProperty方法、get方法, 先来看看set方法, 我们说了好多次第一个映射表, 现在看看它到底怎么实现吧

type classInstance<T = any> = new (...args: any[]) => T;
type ServiceKey<T = any> = string | classInstance<T>;

class Container {
  // provide维护的映射表
  private static services: Map<ServiceKey, classInstance> = new Map();
  static set(key: ServiceKey, value: classInstance) {
    Container.services.set(key, value);
  }
  // ...
}

前面已经解释过了, 第一个映射表的键, 可以是一个类, 也可以是一个字符串格式的自定义键, 所以, 我们索性将其封装为ServiceKey类型

再看看inject的关系映射表, 我们使用了regsiterProperty方法维护它

class Container {
  private static propertyRegisters: Map<string, ServiceKey> = new Map();
  static regsiterProperty(key: string, value: ServiceKey) {
    Container.propertyRegisters.set(key, value);
  }
  // ...
}

这个关系映射表的值就是上一个映射表的键, 所以, 也干脆定义为ServiceKey;

最后, 也是最重要的get方法, 它负责将以上逻辑都整合起来, 它需要通过传入的一个参数, 从第一个映射表中找到对应的类, 然后从第二张关系映射表里找到它所有属性的依赖类, 再将其实例化赋给属性;

class Container {
	static get(key: ServiceKey): any {
    // 从第一张映射表中找到对应的类
    const Cons = Container.services.get(key);
    // 找不到就直接return了
    if (!Cons) return;
    // 找到则实例化
    const ins = new Cons();
    // 获取关系表
    const Relations = Container.propertyRegisters;
    for (const item of Relations) {
      // key就是字符串, 格式为 '类名:属性名'
      // value就是ServiceKey类型
      const [key, value] = item;
      // 类名
      const name = key.split(':')[0];
      // 对比关系映射表的中的类名, 和获取到的类名; 如果不等就直接跳过
      if (Cons.name !== name) continue;
      // 找到对应类的属性名
      const propKey = key.split(':')[1];

      // 将属性所需要的那个类传入get,递归
      const result = Container.get(value);
      if (result) {
        // 赋值给对应的属性
        ins[propKey] = result;
      }
    }
    // 返回
    return ins;
  }
  // ...
}

好了, 我们来看看完整代码

import 'reflect-metadata';
type classInstance<T = any> = new (...args: any[]) => T;
type ServiceKey<T = any> = string | classInstance<T>;

class Container {
  private static services: Map<ServiceKey, classInstance> = new Map();
  static set(key: ServiceKey, value: classInstance) {
    Container.services.set(key, value);
  }
  private static propertyRegisters: Map<string, ServiceKey> = new Map();
  static regsiterProperty(key: string, value: ServiceKey) {
    Container.propertyRegisters.set(key, value);
  }
  static get(key: ServiceKey): any {
    const Cons = Container.services.get(key);
    if (!Cons) return;
    const ins = new Cons();
    const Relations = Container.propertyRegisters;
    for (const item of Relations) {
      const [key, value] = item;
      const name = key.split(':')[0];
      if (Cons.name !== name) continue;
      const propKey = key.split(':')[1];
      const result = Container.get(value);
      if (result) {
        ins[propKey] = result;
      }
    }
    return ins;
  }
}

function Provide(key?: string): ClassDecorator {
  return function (target: any) {
    Container.set(key ?? target.name, target);
    Container.set(target, target);
  };
}

function Inject(key?: string): PropertyDecorator {
  return function (prototype: any, propName: string | symbol) {
    Container.regsiterProperty(
      `${prototype.constructor.name}:${String(propName)}`,
      key ?? Reflect.getMetadata('design:type', prototype, propName)
    );
  };
}

来试试效果如何吧

@Provide('汽油')
class Feul {
  NO: string = '95#';
}

@Provide()
class Car {
  @Inject('汽油')
  feul!: Feul;
  constructor() {}
  run() {
    console.log(this.feul.NO);
  }
}

const car = Container.get(Car);
car.run(); // 95#

案例2, 基于依赖注入的路由

在很多nodeJS的库中, 例如: Nest中就大量使用了装饰器, 例如, 使用装饰器来实现一个路由

@Controller('/public')
class Request {
  @Get('/lists')
  getLists() {
    return new Promise((resolve) => {
      resolve('this is lists');
    });
  }
  @Post('/push')
  push() {
    return new Promise((resolve) => {
      resolve('this is push');
    });
  }
}

即, 路径是一个依赖, 将依赖以参数的形式, 注入到装饰器中, 只有依赖条件都符合了, 即请求路径对了, 才会执行到对应的方法, 我们来看看如何实现它吧

import 'reflect-metadata';

// 请求方法/请求路径元数据的键
enum KEYS {
  MethodKey = 'ioc:method',
  PathKey = 'ioc:path',
}

// 请求方法
enum METHODS {
  GET = 'get',
  POST = 'post',
}

// methodDecorate, 一个偏函数
export function methodDecorate(method: METHODS) {
  return function (path: string): MethodDecorator {
    return function (_target, _methodName, descriptor) {
      Reflect.defineMetadata(KEYS.MethodKey, method, descriptor.value!);
      Reflect.defineMetadata(KEYS.PathKey, path, descriptor.value!);
    };
  };
}

// 确定请求方法, 获得Get/Post装饰器
export const Get = methodDecorate(METHODS.GET);
export const Post = methodDecorate(METHODS.POST);

// 根路径装饰器
export function Controller(rootPath: string): ClassDecorator {
  return function (target: any) {
    Reflect.defineMetadata(KEYS.PathKey, rootPath, target);
  };
}

以上我们先通过methodDecorate这个偏函数, 确定了请求方法, 得到Get和Post这俩方法装饰器, 而装饰器内部, 其实就是将请求方法和请求路径注册为元数据, 挂到方法实体(descriptor.value)上; 然后定义Controller类装饰器, 将根路径注册为类的元数据; 我们先写一个简单的class试试, 看看装饰器是否都能成功注册数据

@Controller('/root')
class Foo {
  @Get('/info')
  getInfo() {}
  @Post('/push')
  upload() {}
}

const foo = new Foo();
console.log(Reflect.getMetadata(KEYS.MethodKey, foo.getInfo)); // get
console.log(Reflect.getMetadata(KEYS.PathKey, foo.getInfo)); // /info
console.log(Reflect.getMetadata(KEYS.MethodKey, foo.upload)); // post
console.log(Reflect.getMetadata(KEYS.PathKey, foo.upload)); // /push

接下来, 我们要做的事情就是, 需要有一个方法, 将这些信息获取并整合

// 我们希望拿到的数据
interface ReqInfo {
  requestMethod: METHODS;
  requestPath: string;
  requestHandler: (...args: any[]) => Promise<void>;
}
// 整合元数据的方法
export function routerFactory(target: any): ReqInfo[] {
  // 获取原型对象
  const prototype = Reflect.getPrototypeOf(target) as any;
  // 获取根路径
  const rootPath = Reflect.getMetadata(KEYS.PathKey, prototype.constructor);
  // 获取实例的方法
  const methodKeys = Reflect.ownKeys(prototype).filter(
    (key) => key !== 'constructor'
  );
  let result: any[] = [];
  methodKeys.forEach((methodKey) => {
    // 获取执行方法
    const requestHandler = prototype[methodKey];
    // 请求路径
    const requestPath = Reflect.getMetadata(KEYS.PathKey, requestHandler);
    // 请求方法
    const requestMethod = Reflect.getMetadata(KEYS.MethodKey, requestHandler);
    result.push({
      requestMethod,
      requestPath: rootPath + requestPath,
      requestHandler: requestHandler,
    });
  });
  return result;
}

现在来用用我们写好的装饰器和组装函数吧

@Controller('/public')
class Request {
  @Get('/lists')
  getLists() {
    return new Promise((resolve) => {
      resolve('this is lists');
    });
  }
  @Post('/push')
  push() {
    return new Promise((resolve) => {
      resolve('this is push');
    });
  }
}

const request = new Request();
const arr = routerFactory(request);
// arr数据如下
/**
 * [
    {
      requestMethod: 'get',
      requestPath: '/public/lists',
      requestHandler: [Function: getLists]
    },
    {
      requestMethod: 'post',
      requestPath: '/public/push',
      requestHandler: [Function: push]
    }
  ]
 */

我们获得了数组arr, 这样, 就可以根据不同的请求, 执行对应的方法了! 我们来搭建个简单的http服务:

import http from 'http';
http
  .createServer((req, res) => {
    // 遍历arr,判断其请求方法/路径是否匹配
    for (let item of arr) {
      if (
        req.url === item.requestPath &&
        req.method === item.requestMethod.toUpperCase()
      ) {
        // 匹配则执行方法
        item.requestHandler().then((result) => {
          res.writeHead(200, { 'Content-Type': 'application/json' });
          res.end(result);
        });
      }
    }
  })
  .listen(3000)
  .on('listening', () => {
    console.log('监听成功');
  });

来看看实际页面的结果

这样, 我们就实现了一个基于依赖注入的路由了, 我们只需要在一个类上定义装饰器, 就能实现路由功能了!