循环依赖 (Circular Dependency)
解开编程中的“死结”:NestJS 循环依赖完全指南
想象一个有点滑稽的办公室场景:
新来的员工小 A 需要他的直属上司 B 签字,才能激活他的员工账户。但公司的规定是,上司 B 必须先用自己的账户登录系统,才能为下属签字。而今天 B 的账户也恰好被锁了,需要下属 A 登录后帮他确认身份才能解锁。
于是,一个死结出现了:
- A 需要 B 先完成操作。
- B 需要 A 先完成操作。
他们两个互相等待,谁也无法继续工作。这就是一个循环依赖。
在 NestJS(乃至许多依赖注入框架)中,当两个类(或模块)在它们的构造函数中直接或间接地互相需要对方时,就会发生同样的情况。NestJS 的依赖注入容器在启动时会尝试构建依赖关系图,当它遇到这种“我需要你,你需要我”的死循环时,它会感到困惑并抛出错误,因为它不知道该先创建哪一个。
1. 制造一个“死结”:服务之间的循环依赖
让我们亲手写一个循环依赖的例子,看看它是什么样子,以及 NestJS 会给我们什么提示。
第一步:创建两个互相需要的服务
src/cats/cats.service.ts
import { Injectable } from '@nestjs/common';
import { DogsService } from '../dogs/dogs.service';
@Injectable()
export class CatsService {
// CatsService 需要 DogsService
constructor(private readonly dogsService: DogsService) {}
meow() {
return 'Meow!';
}
barkFromDog() {
// 它想调用狗狗的服务
return `My friend a dog says: ${this.dogsService.bark()}`;
}
}
src/dogs/dogs.service.ts
import { Injectable } from '@nestjs/common';
import { CatsService } from '../cats/cats.service';
@Injectable()
export class DogsService {
// DogsService 也需要 CatsService
constructor(private readonly catsService: CatsService) {}
bark() {
return 'Woof!';
}
meowFromCat() {
// 它也想调用猫猫的服务
return `My friend a cat says: ${this.catsService.meow()}`;
}
}
第二步:在模块中注册它们
src/app.module.ts
import { Module } from '@nestjs/common';
import { CatsService } from './cats/cats.service';
import { DogsService } from './dogs/dogs.service';
@Module({
providers: [CatsService, DogsService],
})
export class AppModule {}
第三步:启动应用,迎接错误!
当你运行 npm run start:dev
,你的应用会立即崩溃,并抛出一个长长的错误,其中最关键的信息是:
Nest can't resolve dependencies of the DogsService (?). Please make sure that the argument CatsService at index [0] is available in the AppModule context.
Potential solutions:
- If CatsService is a provider, is it part of the current AppModule?
- If CatsService is exported from a separate @Module, is that module imported within AppModule?
...
Potential circular dependency detected:
AppModule -> DogsService -> CatsService -> DogsService
NestJS 非常友好地告诉了我们问题所在:Potential circular dependency detected
(检测到潜在的循环依赖),并指出了循环路径:DogsService
需要 CatsService
,而 CatsService
又需要 DogsService
。
2. 解开死结的“金钥匙”:forwardRef()
为了解决这个问题,我们需要有一种方法来告诉 NestJS:“嘿,我知道我现在需要 CatsService
,但你先别急着去创建它,把它当作一个‘待办事项’。等你有空把它创建好之后,再把它交给我。”
这个“待办事项”或者说“转发引用”的机制,就是 forwardRef()
。
forwardRef()
是一个工具函数,它接收一个返回类名的箭头函数 (() => ClassName
)。它能延迟对类名的解析,直到所有类都已经被定义。这打破了同步实例化的死循环。
如何使用 forwardRef()
我们需要在循环依赖的两端都使用它。
第一步:在注入点使用 forwardRef
我们需要修改两个服务的构造函数,使用 @Inject()
装饰器和 forwardRef()
。
src/cats/cats.service.ts
(修复后)
// 1. 从 @nestjs/common 导入 forwardRef 和 Inject
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { DogsService } from '../dogs/dogs.service';
@Injectable()
export class CatsService {
constructor(
// 2. 使用 @Inject 和 forwardRef 包装依赖
@Inject(forwardRef(() => DogsService))
private readonly dogsService: DogsService
) {}
// ... 其他代码不变
meow() {
return 'Meow!';
}
barkFromDog() {
return `My friend a dog says: ${this.dogsService.bark()}`;
}
}
src/dogs/dogs.service.ts
(修复后)
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { CatsService } from '../cats/cats.service';
@Injectable()
export class DogsService {
constructor(
@Inject(forwardRef(() => CatsService))
private readonly catsService: CatsService
) {}
// ... 其他代码不变
bark() {
return 'Woof!';
}
meowFromCat() {
return `My friend a cat says: ${this.catsService.meow()}`;
}
}
现在,当你再次启动应用时,它会顺利启动!forwardRef()
成功地解开了这个死结。
3. 模块之间的循环依赖
循环依赖不仅会发生在服务之间,也会发生在模块之间。
例如,CatsModule
导出了 CatsService
,并且 imports
了 DogsModule
。同时,DogsModule
导出了 DogsService
,并且 imports
了 CatsModule
。
src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsService } from './cats.service';
import { DogsModule } from '../dogs/dogs.module';
@Module({
imports: [DogsModule], // 需要狗狗模块
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
src/dogs/dogs.module.ts
import { Module } from '@nestjs/common';
import { DogsService } from './dogs.service';
import { CatsModule } from '../cats/cats.module';
@Module({
imports: [CatsModule], // 需要猫猫模块
providers: [DogsService],
exports: [DogsService],
})
export class DogsModule {}
这同样会造成一个无法启动的死循环。解决方法完全相同:在 @Module()
装饰器的 imports
数组中使用 forwardRef()
。
src/cats/cats.module.ts
(修复后)
import { Module, forwardRef } from '@nestjs/common';
// ...
@Module({
imports: [forwardRef(() => DogsModule)], // 使用 forwardRef
// ...
})
export class CatsModule {}
src/dogs/dogs.module.ts
(修复后)
import { Module, forwardRef } from '@nestjs/common';
// ...
@Module({
imports: [forwardRef(() => CatsModule)], // 使用 forwardRef
// ...
})
export class DogsModule {}
4. 一个善意的警告:循环依赖是“代码异味”
虽然 NestJS 提供了 forwardRef()
这个强大的工具来解决循环依赖,但循环依赖的出现,通常是你代码设计上一个危险的信号,我们称之为“代码异味 (Code Smell)”。
它往往意味着:
- 职责不清:这两个互相依赖的服务(或模块)的职责划分可能不清晰。它们之间的关系过于紧密,也许它们本应该被合并成一个更大的服务。
- 耦合度过高:你的代码模块像一盘意大利面一样缠绕在一起,修改其中一个很容易影响另一个,难以维护和测试。
在伸手去用 forwardRef()
之前,请先问自己几个问题:
- 我真的需要
A
调用B
,同时B
又调用A
吗? - 有没有可能提取出一个第三方的、更高层次的
C
服务来协调A
和B
?这样A
和B
就不需要直接互相了解了。 - 我是不是违反了单一职责原则?
重构代码,消除循环依赖,通常是比使用 forwardRef()
更好的选择。
总结
循环依赖是 NestJS(及许多框架)中的一个经典问题,但并不可怕。
- 它是什么:两个或多个提供者(或模块)形成了一个相互依赖的闭环。
- 如何解决:使用
forwardRef()
函数来包裹循环依赖的一方或双方,打破同步的依赖链。 - 最佳实践:尽可能避免它!将循环依赖的出现视为一个重新思考和重构代码设计的机会。
forwardRef()
是你的“救生筏”,但你更应该学会如何避免“掉进水里”。