Skip to content

循环依赖的三种解法: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 -> UserService

1.2 循环依赖的危害

循环依赖会导致以下问题:

  1. 启动失败:NestJS 无法解析依赖关系,应用启动失败
  2. 内存泄漏:在某些情况下可能导致内存泄漏
  3. 难以维护:代码结构复杂,难以理解和维护
  4. 测试困难:难以进行单元测试

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 应用开发中的常见问题,但通过合理的设计和适当的解决方案可以有效处理:

  1. forwardRef:最常用的解法,适用于服务间的简单循环依赖
  2. 模块拆分:通过重构改善架构设计,从根本上解决问题
  3. 延迟注入:提供最大的灵活性,但会失去一些编译时检查

在实际开发中,建议:

  1. 优先考虑通过重构设计来避免循环依赖
  2. 必要时使用 forwardRef 解决服务间的循环依赖
  3. 对于模块间的循环依赖,优先考虑模块拆分
  4. 建立代码审查机制,及早发现和处理循环依赖问题

理解这三种解法的原理和适用场景,有助于我们:

  1. 快速识别和解决循环依赖问题
  2. 设计更合理的应用架构
  3. 提高代码的可维护性和可测试性

在下一篇文章中,我们将探讨 Scope:默认 singleton 之外,request-scoped 实例如何工作,了解每次请求都新建实例的内存开销与性能权衡。