Lucas Liao's Blog

如何理解依赖注入?

前言

在过往工作中,使用了nestjs作为后端服务开发框架。nestjs其中显著的特点是module, service,controller之间的依赖注入,本文记录下对依赖注入原理的学习哈,加强记忆。

什么是依赖注入?

依赖注入是控制反转的一种技术实现。

而控制反转是什么东东呢?举个例子:

classA -> classB

classA内部使用classB的某个方法,在常规的编程下,我们一般通过以下方式进行创建:

1
2
3
4
5
6
7
8
9
10
class A {
B: IB
constructor(xxx) {
this.B = new IB(xxx)
}
dosomething() {
this.B.dosomethingofB()
}

}

被依赖的类会在constructor函数进行创建,并且传入相应的参数。

从上面的demo我们可以看出,需要由调用方主动去创建依赖方的实例,并且需要保证所需的参数结构。

这时候我们思考一个问题,在大型的开发项目中,使用面向对象的编程思维,必然会存在错综复杂的依赖关系,所以我们需要维护两个关键代码:

  • 实例的创建
  • 依赖参数的改动维护

因此,控制反转其实就是为了解决以上问题。

在控制反转的模式下,我们的代码将会变成如下方式:

1
2
3
4
5
6
class A {
constructor(B: IB) {}
dosomething() {
this.B.dosomethingofB()
}
}

我们可以看到省了classB的创建过程,不需要我们主动去创建classB,而是自动被注入到classA当中,这就是依赖注入基于控制反转设计模式下的实现,目的是就是为了解耦。

简单来说,依赖注入就是将一个对象所依赖的其他对象通过构造函数、方法参数或者属性注入到该对象中,而不是在该对象内部直接创建这些依赖对象。这样做的好处是,我们可以更容易地替换依赖对象,从而实现代码的可测试性和可扩展性。

nestjs中的实现原理

从上面的demo我们只在constructor中传入B的类型,但是typescript只能是在编译是进行静态检查,如何在运行时获取相应classB的类型呢?

这利用到ES7提出的Reflect metadata特性,允许在声明时候添加或获取实例的元数据,具体介绍可以Google了解学习下。

它提供如下3种使用场景:

design:type — 获取属性类型
design:paramtypes — 获取方法参数类型
design:returntype — 获取方法返回类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class User () {
@Inject()
admin: Admin
work (time: Time): Boolean {
this.admin.open()
}
}

console.log(Reflect.getMetadata('design:type', User, 'admin'));
// Admin
console.log(Reflect.getMetadata('design:paramtypes', User, 'work'));
// [Time]
console.log(Reflect.getMetadata('design:returntype', User, 'work'));
// Boolean

那么如果通过Relfect实现一个依赖注入呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require("reflect-metadata");

type Constructor<T = any> = new (...args: any[]) => T;
const Injectable = (): ClassDecorator => (target) => {};
class OtherService {
a = "a property from other service";
}
@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

测试地址:https://codesandbox.io/s/lingering-moon-xcw6mj?file=/src/index.ts

在nestjs中,搭配使用@Injectable@Inject()装饰器将class统一放到全局的IOC Container进行管理。启动服务时候,依次去取出class相应的依赖进行实例化。在实际内部中,需要维护class之间的状态,避免重复创建,单例模式等。

有什么坑?

在pnpm monorepo下,主项目通过import方式引入libs下某个nestjs模块,在这个场景下主项目的@nestjs/core与libs的@nestjs/core不是指向同一个内存,在编译阶段导致启动失败。

解决方法:将主项目node_moudles下的libs指向硬链到项目代码具体的libs位置。