循环依赖的三种解法:forwardRef、模块拆分、延迟注入
在 NestJS 应用开发中,循环依赖是一个常见但棘手的问题。当两个或多个模块或服务相互依赖时,就会形成循环依赖,这可能导致应用无法正常启动或运行时出现错误。NestJS 提供了多种解决方案来处理循环依赖问题,其中最常用的是 forwardRef,但还有其他方法如模块拆分和延迟注入。本文将深入探讨这三种解法的原理和使用场景。
1. 循环依赖基础概念
1.1 什么是循环依赖?
循环依赖是指两个或多个组件相互依赖,形成一个闭环依赖关系:
typescript
// user.service.ts
@Injectable()
export class UserService {
constructor(private orderService: OrderService) {} // 依赖 OrderService
}
// order.service.ts
@Injectable()
export class OrderService {
constructor(private userService: UserService) {} // 依赖 UserService
}
// 这就形成了循环依赖:UserService -> OrderService -> UserService1.2 循环依赖的危害
循环依赖会导致以下问题:
- 启动失败:NestJS 无法解析依赖关系,应用启动失败
- 内存泄漏:在某些情况下可能导致内存泄漏
- 难以维护:代码结构复杂,难以理解和维护
- 测试困难:难以进行单元测试
2. forwardRef 解法
2.1 forwardRef 基本用法
forwardRef 是 NestJS 提供的解决循环依赖的主要方法:
typescript
// user.service.ts
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => OrderService))
private orderService: OrderService
) {}
}
// order.service.ts
@Injectable()
export class OrderService {
constructor(
@Inject(forwardRef(() => UserService))
private userService: UserService
) {}
}
// user.module.ts
@Module({
providers: [UserService, OrderService],
exports: [UserService, OrderService],
})
export class UserModule {}2.2 forwardRef 工作原理
typescript
// nestjs/common/inject.decorator.ts(简化版)
export function Inject(token: any): PropertyDecorator & ParameterDecorator {
return (target: object, key: string | symbol | undefined, index?: number) => {
// 处理 forwardRef
if (isForwardReference(token)) {
Reflect.defineMetadata(
`${FORWARD_REF_METADATA}:${key || index}`,
token.forwardRef,
target,
);
} else {
Reflect.defineMetadata(
`${INJECT_METADATA}:${key || index}`,
token,
target,
);
}
};
}
// nestjs/common/forward-ref.interface.ts
export interface ForwardReference {
forwardRef: () => any;
}
// nestjs/common/forward-ref.ts
export function forwardRef(fn: () => any): ForwardReference {
return { forwardRef: fn };
}2.3 依赖解析过程
typescript
// 简化的依赖解析过程
class DependenciesScanner {
private async scanForDependencies(wrapper: InstanceWrapper) {
const tokens = this.getInjectionTokens(wrapper.metatype);
for (const token of tokens) {
if (isForwardReference(token)) {
// 延迟解析依赖
wrapper.forwardRefTokens.push(token.forwardRef());
} else {
// 立即解析依赖
const dependency = this.getProvider(token);
wrapper.dependencies.push(dependency);
}
}
}
private async resolveForwardReferences(wrapper: InstanceWrapper) {
// 在所有模块扫描完成后,解析 forwardRef 依赖
for (const forwardRefToken of wrapper.forwardRefTokens) {
const token = forwardRefToken();
const dependency = this.getProvider(token);
wrapper.dependencies.push(dependency);
}
}
}2.4 使用场景和限制
typescript
// 适用于服务间的循环依赖
@Injectable()
export class AuthService {
constructor(
@Inject(forwardRef(() => UserService))
private userService: UserService
) {}
async validateUser(userId: string) {
// 使用 UserService
return this.userService.findById(userId);
}
}
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => AuthService))
private authService: AuthService
) {}
async findById(id: string) {
// 可能需要验证用户权限
await this.authService.validateUser(id);
// 返回用户信息
return { id, name: 'User' };
}
}3. 模块拆分解法
3.1 模块拆分原理
通过将共同依赖提取到独立模块来打破循环依赖:
typescript
// ❌ 错误的循环依赖
// user.module.ts
@Module({
imports: [OrderModule],
providers: [UserService],
})
export class UserModule {}
// order.module.ts
@Module({
imports: [UserModule], // 循环依赖
providers: [OrderService],
})
export class OrderModule {}
// ✅ 正确的模块拆分
// shared/user-order.models.ts
export interface User {
id: string;
name: string;
}
export interface Order {
id: string;
userId: string;
}
// user-order.service.ts
@Injectable()
export class UserOrderService {
private users: User[] = [];
private orders: Order[] = [];
findUserById(id: string): User | undefined {
return this.users.find(user => user.id === id);
}
findOrdersByUserId(userId: string): Order[] {
return this.orders.filter(order => order.userId === userId);
}
}
// shared.module.ts
@Module({
providers: [UserOrderService],
exports: [UserOrderService],
})
export class SharedModule {}
// user.module.ts
@Module({
imports: [SharedModule],
providers: [UserService],
})
export class UserModule {}
// order.module.ts
@Module({
imports: [SharedModule],
providers: [OrderService],
})
export class OrderModule {}3.2 领域驱动设计(DDD)方法
typescript
// 使用聚合根概念避免循环依赖
// user.aggregate.ts
export class User {
private orders: Order[] = [];
addOrder(order: Order) {
this.orders.push(order);
order.setUserId(this.id);
}
getOrders(): Order[] {
return this.orders;
}
}
// order.aggregate.ts
export class Order {
private userId: string;
setUserId(userId: string) {
this.userId = userId;
}
getUserId(): string {
return this.userId;
}
}
// user.service.ts
@Injectable()
export class UserService {
constructor(private userOrderService: UserOrderService) {}
createUser(userData: any): User {
const user = new User(userData);
// 保存用户
return user;
}
getUserWithOrders(userId: string) {
const user = this.userOrderService.findUserById(userId);
const orders = this.userOrderService.findOrdersByUserId(userId);
return { user, orders };
}
}4. 延迟注入解法
4.1 延迟注入原理
通过在方法内部而不是构造函数中注入依赖来避免循环依赖:
typescript
// user.service.ts
@Injectable()
export class UserService {
private orderService: OrderService;
constructor(private moduleRef: ModuleRef) {}
private getOrderService(): OrderService {
if (!this.orderService) {
this.orderService = this.moduleRef.get(OrderService);
}
return this.orderService;
}
async getUserOrders(userId: string) {
const orderService = this.getOrderService();
return orderService.findByUserId(userId);
}
}
// order.service.ts
@Injectable()
export class OrderService {
private userService: UserService;
constructor(private moduleRef: ModuleRef) {}
private getUserService(): UserService {
if (!this.userService) {
this.userService = this.moduleRef.get(UserService);
}
return this.userService;
}
async createOrder(userId: string, orderData: any) {
const userService = this.getUserService();
await userService.validateUser(userId);
// 创建订单逻辑
return { id: 'order1', ...orderData };
}
}4.2 ModuleRef 的使用
typescript
// 使用 ModuleRef 实现延迟注入
@Injectable()
export class UserService {
constructor(private moduleRef: ModuleRef) {}
async processUserOrder(userId: string) {
// 在需要时动态获取依赖
const orderService = this.moduleRef.get(OrderService);
const paymentService = this.moduleRef.get(PaymentService);
const user = await this.findById(userId);
const orders = await orderService.findByUserId(userId);
for (const order of orders) {
await paymentService.processPayment(order);
}
return { user, orders };
}
private findById(id: string) {
return Promise.resolve({ id, name: 'User' });
}
}5. 三种解法的比较
5.1 适用场景对比
| 解法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| forwardRef | 服务间循环依赖 | 简单直接,保持依赖注入 | 运行时解析,可能隐藏设计问题 |
| 模块拆分 | 模块间循环依赖 | 改善架构设计,提高可维护性 | 需要重构代码结构 |
| 延迟注入 | 复杂依赖关系 | 灵活性高,按需加载 | 失去编译时检查,增加复杂性 |
5.2 最佳实践建议
typescript
// 1. 优先考虑重构设计
// 不好的设计
@Injectable()
export class UserService {
constructor(private orderService: OrderService) {}
}
@Injectable()
export class OrderService {
constructor(private userService: UserService) {}
}
// 好的设计
@Injectable()
export class UserOrderService {
findUserOrders(userId: string) {
// 统一处理用户和订单关系
}
}
@Injectable()
export class UserService {
constructor(private userOrderService: UserOrderService) {}
}
@Injectable()
export class OrderService {
constructor(private userOrderService: UserOrderService) {}
}6. 高级用法
6.1 条件性 forwardRef
typescript
// 根据环境决定是否使用 forwardRef
@Injectable()
export class UserService {
constructor(
@Inject(
process.env.NODE_ENV === 'test'
? forwardRef(() => MockOrderService)
: OrderService
)
private orderService: OrderService
) {}
}6.2 复杂循环依赖处理
typescript
// 处理多个服务间的复杂循环依赖
@Injectable()
export class ServiceA {
constructor(
@Inject(forwardRef(() => ServiceB))
private serviceB: ServiceB,
@Inject(forwardRef(() => ServiceC))
private serviceC: ServiceC
) {}
}
@Injectable()
export class ServiceB {
constructor(
@Inject(forwardRef(() => ServiceC))
private serviceC: ServiceC,
@Inject(forwardRef(() => ServiceA))
private serviceA: ServiceA
) {}
}
@Injectable()
export class ServiceC {
constructor(
@Inject(forwardRef(() => ServiceA))
private serviceA: ServiceA,
@Inject(forwardRef(() => ServiceB))
private serviceB: ServiceB
) {}
}7. 预防循环依赖
7.1 设计原则
typescript
// 1. 使用分层架构
// controller layer -> service layer -> repository layer -> external services
// 2. 依赖倒置原则
export abstract class UserStorage {
abstract save(user: User): Promise<void>;
abstract findById(id: string): Promise<User>;
}
@Injectable()
export class DatabaseUserStorage implements UserStorage {
// 实现细节
}
@Injectable()
export class UserService {
constructor(private userStorage: UserStorage) {} // 依赖抽象而不是具体实现
}7.2 代码审查检查点
typescript
// 检查循环依赖的工具函数
function detectCircularDependencies(modules: Module[]) {
// 实现循环依赖检测逻辑
// 可以在测试或构建时运行
}8. 总结
循环依赖是 NestJS 应用开发中的常见问题,但通过合理的设计和适当的解决方案可以有效处理:
- forwardRef:最常用的解法,适用于服务间的简单循环依赖
- 模块拆分:通过重构改善架构设计,从根本上解决问题
- 延迟注入:提供最大的灵活性,但会失去一些编译时检查
在实际开发中,建议:
- 优先考虑通过重构设计来避免循环依赖
- 必要时使用 forwardRef 解决服务间的循环依赖
- 对于模块间的循环依赖,优先考虑模块拆分
- 建立代码审查机制,及早发现和处理循环依赖问题
理解这三种解法的原理和适用场景,有助于我们:
- 快速识别和解决循环依赖问题
- 设计更合理的应用架构
- 提高代码的可维护性和可测试性
在下一篇文章中,我们将探讨 Scope:默认 singleton 之外,request-scoped 实例如何工作,了解每次请求都新建实例的内存开销与性能权衡。