TS 装饰器实现简单的依赖注入

240 阅读3分钟

控制反转

按照维基百科,IoC(Inversion of Control)控制反转,是面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。先来了解一下控制反转可以帮助我们解决什么问题。假如现在有两个类 A 和 B。依赖关系如下:

graph LR

A-->B

代码如下:

class A {
 private b: B;

 constructor() {
  this.b = new B();
 }
}

class B {
 constructor() {}
}

假如现在需要对 B 进行修改,在 constructor 中增加参数,那么需要做的修改如下:

class A {
 private b: B;

 constructor(name: string) {
  // 实例化需要增加参数 name
  this.b = new B(name);
 }
}

class B {
 private name: string;

 // 增加参数 Name
 constructor(name: string) {
  this.name = name;
 }
}

可以看到对 B 的修改会影响到使用它的 A ,如果所有的依赖都是按照这种方式构建的,那么当类的数量多起来,每一次修改都需要修改多个地方,维护难度会非常大。

那么如何可以解决这个问题呢?最简单的做法是把 B 实例化以后作为 A constructor 的参数传入即可。

代码变更为:

class A {
 private b: B;

 // 直接传入 b 的实例
 constructor(b: B) {
  this.b = b;
 }
}

class B {
 private name: string;

 // 增加参数 Name
 constructor(name: string) {
  this.name = name;
 }
}

这就是控制反转带来的好处,以后 A 就不需要关心 B 的实现细节了。但是这样好像还不够好,哪一天我需要修改 A 时候是不是也要把实例化 A 的代码都修改一遍。那有没有一种办法可以把 B 实例直接赋值到定义的属性上呢?通过 TS 修饰器实现的依赖注入是可以的。

依赖注入

需要使用到的库

npm i reflect-metadata --save

在项目中引入依赖

// index.ts
import "reflect-metadata";

基本结构图

graph LR

Service[Service]--往 Container 注册类-->Container[Container 保存类和实例]
Container[Container 保存类和实例]--从 Container 获取注册的类-->Inject[Inject]

代码实现

// index.ts
import "reflect-metadata";

class Container {
 private ContainerMap = new Map<string | symbol, any>();
 public set = (id: string | symbol, value: any): void => {
  this.ContainerMap.set(id, value);
 };

 public get = <T extends any>(id: string | symbol): T => {
  return this.ContainerMap.get(id) as T;
 };

 public has = (id: string | symbol): Boolean => {
  return this.ContainerMap.has(id);
 };
}

const ContainerInstance = new Container();

interface ConstructableFunction extends Function {
 new (...args): any;
}

export function Service(config?: {
 id?: string;
 // 默认以单例的方式保存
 singleton?: boolean;
}): Function {
 return (target: ConstructableFunction) => {
  const { id, singleton = true } = config || {};
  let singleInstance;

  // 没有 id 生成一个 symbol 的 id
  const serviceId = id || Symbol(target.name);
  if (typeof serviceId === "string" && ContainerInstance.has(serviceId)) {
   throw new Error(`${serviceId} has been used`);
  }

  Reflect.defineMetadata("custom:id", serviceId, target);
  // 获取 construct 的参数类型
  const args = Reflect.getMetadata("design:paramtypes", target) as any[];
  if (singleton) {
   // 获取对应的参数实例传入
   const instances =
    args?.map((arg) => {
     const id = Reflect.getMetadata("custom:id", arg);
     const instance = ContainerInstance.get(id as any);
     return instance;
    }) || [];
   singleInstance = new target(...instances);
  }
  ContainerInstance.set(serviceId, singleInstance || target);
 };
}

// 使用 id 定义模块后,需要使用 id 来注入模块
export function Inject(id?: any): PropertyDecorator {
 return (target: Object, propertyKey: string | symbol) => {
  const Dependency = Reflect.getMetadata("design:type", target, propertyKey);
  const serviceId = id || Reflect.getMetadata("custom:id", Dependency as any);
  const dependency = ContainerInstance.get(serviceId as any);
  // 给属性注入依赖
  Object.defineProperty(target, propertyKey, {
   value: dependency,
  });
 };
}

@Service()
class A {
 public main(...args: any[]): void {
  console.info("I am A", ...args);
 }
}

@Service()
class B {
 public main(...args: any[]): void {
  console.info("I am B", ...args);
 }
}

@Service()
class CustomerController {
 // 使用 Inject 注入
 @Inject()
 private a!: A;

 // constructor 的参数自动注入
 constructor(private b: B) {
  this.b.main(" bbb ");
  this.main();
 }

 public main(): void {
  this.a.main(" aaa ");
 }
}

存在的问题

  1. @Service 的注册需要依次注册,不然使用会报错。(可以先收集依赖,根据依赖树来决定实例化顺序)
  2. 不支持依赖的懒加载

参考文章

juejin.cn/post/687223…