Nest系列概念篇,聊聊基础核心思想。
设计思想
IOC控制反转是一种程序设计的思想,将对象的创建和调用管理转移给了容器来实现,核心思想是将控制权从调用者反转给容器,即调用者控制对象的创建和管理转变为容器统一控制对象的创建和管理。
不理解?举个生活中的栗子🌰:
有一天我想吃饺子了,首先需要去肉档里面买新鲜的瘦肉,去手工饺店买饺子皮,还要去菜市场里面买韭菜、玉米,然后回家一顿操作终于吃上了热乎乎的饺子🥟。
基于我有超高的厨艺天赋,第二天还想吃,但突然发现我依赖的手工饺店倒闭了,这时候需要费很大功夫去很远的地方才能买到新的饺子皮,整个人都emo了,吃完我还得继续干活呢。。。
解决这个问题的方法是找一家餐馆,看着菜单点一份饺子:"我需要鲜肉玉米饺子,谢谢!"。
这时候,餐馆 就是一个IOC容器,我们把制作饺子整个过程的控制权交给了餐馆去处理,不用担心菜市场是否关门、肉档的瘦肉是否卖完了,我只需要做的事情就是填饱肚子拍屁股走人。
三个关键点
1. 控制反转
传统的编程模式中,对象的创建和依赖是由使用对象的代码来控制,需要知道什么时候创建/使用对象,而在IOC中,控制权交给了容器,负责创建对象并且解决对象之间的依赖关系,使得在修改某一类的时候,调用者不需要重新修改其他创建对象的地方,实现了对象之间的松耦合。
2. 依赖注入
控制反转是一种思想,它告诉我们解决问题的思路,但没有具体告诉我们应该怎么做。而依赖注入,就是实现IOC的方式,通过依赖注入(DI),容器将依赖关系自动注入到指定对象中,而不是让对象自己去创建或者查找其他依赖的对象。实现方式可以是通过构造函数注入、属性注入或者方法注入。
再通过餐馆的例子来解释一下:
- 容器即餐馆
- 依赖即各种口味的饺子
- 对象即顾客
我们尝试用白话版解释一下这句话:
容器将依赖关系自动注入到指定对象中,而不是让对象自己去创建或者查找其他依赖的对象。
翻译为:
餐馆通过把菜单注入给指定顾客,顾客可以通过菜单来选择喜欢吃的口味进行下单,而不是尝试顾客自己去找食材递给厨师,看着他做。
关于依赖注入,还需要补充一点的就是,在我们熟悉的Vue中也体现了这一点:Inject和Provider。
在组件通信中,祖先组件中提供依赖给后代组件使用,后代无需关心祖先组件是如何创建和销毁依赖。
在Nest中是通过装饰器的方式实现依赖注入,开发者只需要关注自己的业务逻辑,而不需要关心如何/何时创建或依赖对象。
关于装饰器概念,第二章节中会详细讲解,敬请期待!
3. 容器
IOC中的容器可以理解为依赖调度中心,负责管理所有的对象生命周期管理和依赖关系,在Nest中有内置的IOC容器,它是IOC的核心。通过配置中心或注解的方式来定义对象之间的依赖关系。
示例
我们通过实际的例子来展示:
创建一个公共类 A, 业务类 B 和 业务类 C 分别依赖 A
typescript
// 公共类
class A {
name: string
constructor() {
this.name = "jmin"
}
}
// 业务类
class B {
b: string
constructor() {
this.b = new A().name
}
}
// 业务类
class C {
c: string
constructor() {
this.c = new A().name
}
}
当存在这种依赖关系的时候,尝试修改一下 公共类A,将name改为动态赋值。
typescript
class A {
name: string
constructor(name: string) {
this.name = name
}
}
那么,这时候依赖公共类的业务类B和C都需要在对象创建的时候修改创建方式,在大型应用中,如果有100个业务类依赖公共类A呢?
而IOC/DI为了解决这个问题应运而生。
我们来看下通过IOC方式维护的示例:
typescript
class A1 {
name: string
constructor(name: string) {
this.name = name
}
}
class B1 {
name: string
constructor(name: string) {
this.name = name
}
}
// 依赖容器
class Container {
map: Record<string, any>
constructor() {
this.map = {}
}
provider(name: string, obj: unknown) {
this.map[name] = obj
}
get(name: string) {
return this.map[name]
}
}
// 新建容器进行收集引用
const container = new Container()
// 注入依赖类
container.provider('A1', new A1('jmin'))
container.provider('B1', new B1('hide on home'))
// C1 这个类就可以随意在容器中获取需要依赖的类
class C1 {
a: unknown
b: unknown
constructor(container: Container) {
this.a = container.get('A1')
this.b = container.get('B1')
}
}
上述代码中,容器中维护着多个公共类A1、B1,它们在某个时机统一注册到容器中(依赖注入),而此时业务类C1可以随意在容器中获取想要依赖的类,并且当A1、B1改变,不需要修改到调用方,实现逻辑解耦。
好了,回过神来,在Nest中,IOC容器是内置的nest运行时系统,那我们如何向容器注册依赖呢?
答案是module。
typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { AController } from './A/A.controller';
import { AService } from './A/A.service';
@Module({
controllers: [AController],
providers: [AService],
})
export class AppModule {}
module可以接受多个诸如controllers、providers的依赖。
那么,该如何定义一个提供者provider呢? 通过@Injectable()装饰器将service类标识为提供者,此时,IOC容器将会接管这个类的控制权。
typescript
// a.service.ts
import { Injectable } from '@nestjs/common';
import { A } from './interfaces/A.interface';
@Injectable()
export class AService {
private readonly a: A[] = [];
findAll(): A[] {
return this.a;
}
}
一般情况下,Controller不做具体业务逻辑,它只负责分发请求,具体的请求逻辑由Service提供者管理,所以我们将提供者注入到控制器中,通过构造函数注入声明了对AService的依赖。
typescript
// a.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AService } from './A.service';
import { A } from './interfaces/A.interface';
@Controller('A')
export class AController {
constructor(private aService: AService) {}
@Get()
async findAll(): Promise<A[]> {
return this.aService.findAll();
}
}
总体的注册过程如下: 当 Nest IoC 容器实例化一个 AController 时,它首先查找任何依赖。 当它找到 AService 依赖时,它会根据注册步骤对 AService 令牌执行查找,返回AService 类。
好了,这里的AService令牌是什么玩意?
首先,这里存在一个语法糖,即:
typescript
providers: [AService],
// 等同于
providers: [
{
provide: AService,
useClass: AService,
}
]
而这里的provide就是标记提供者的令牌,它可以是字符串或者其他类型的值。
所以,Controller中查找依赖是通过这个令牌去容器中查找的。
这就是Nest中依赖注入(注册)、依赖查找的大概过程,了解了这些基本概念,对后面的知识点会更加容易些。
总结
本章讲了Nest中基本的核心概念,通过类比的方式能够方便理解什么是IOC和DI,最后介绍Nest中依赖注入的方式和基本原理。
注:我尽可能尝试用白话版的方式翻译过来,以便大家理解,如有不恰当的地方,恳请指出!!!