Nestjs:什么是循环依赖?又如何解决它?

399 阅读3分钟

在使用 NestJS 开发应用时,我们常常会遇到一个棘手的问题:循环依赖。这种情况不仅会导致应用无法正常启动,还会给开发者带来困惑。在本文中,我们将探讨什么是循环依赖,它的成因,以及如何在 NestJS 中解决它。

什么是循环依赖?

循环依赖报错如下图所示:

image.png

image.png

循环依赖是指两个或多个moduleprovider互相依赖,形成一个闭环。举个例子,假设我们有两个服务(Service),AServiceBServiceAService 依赖 BService,同时 BService 也依赖 AService。这两,互相等待对方完成。这时候如果不做处理,会出现“鸡生蛋还是蛋生鸡”的问题,即 NestJS 不知道先创建哪个服务。

// a.module.ts
import { Module } from '@nestjs/common';
import { AService } from './a.service';
import { AController } from './a.controller';
import { BService } from '../b/b.service';

@Module({
  controllers: [AController],
  providers: [AService, BService],
})
export class AModule {}

// a.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { BService } from './b.service';

@Injectable()
export class AService {
  constructor(@Inject(forwardRef(() => BService)) private readonly bService: BService) {}
}

// b.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { AService } from './a.service';

@Injectable()
export class BService {
  constructor(@Inject(forwardRef(() => AService)) private readonly aService: AService) {}
}

// b.module.ts
import { Module } from '@nestjs/common';
import { BService } from './b.service';
import { BController } from './b.controller';
import { AService } from '../a/a.service';

@Module({
  controllers: [BController],
  providers: [BService, AService],
})
export class BModule {}

为什么会产生循环依赖?

在开发过程中,循环依赖通常是因为以下几种情况导致的:

  1. 设计不当:当两个或多个模块过于紧密耦合时,就容易产生循环依赖。这通常是因为模块之间缺乏清晰的边界,导致它们需要彼此的功能。
  2. 功能扩展:随着项目的发展,最初没有循环依赖的模块之间可能会因为新功能的添加而产生依赖关系,从而形成循环。
  3. 缺乏中间层:有时,直接依赖另一个模块的某些功能而不使用接口或抽象类,会导致循环依赖。如果引入一个中间层来管理这些依赖关系,可以有效减少循环依赖的可能性。

如何解决循环依赖?

1、使用 forwardRef()

forwardRef() 是 NestJS 提供的一个工具,用来解决循环依赖问题。它的作用是推迟依赖项的解析,直到所有相关依赖都被定义好为止。简单来说,它告诉 NestJS:“稍后再去解析这个依赖”。

// a.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { BService } from './b.service';

@Injectable()
export class AService {
  constructor(@Inject(forwardRef(() => BService)) private readonly bService: BService) {}
}

// b.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common';
import { AService } from './a.service';

@Injectable()
export class BService {
  constructor(@Inject(forwardRef(() => AService)) private readonly aService: AService) {}
}

在上面的例子中,我们在 AServiceBService 的构造函数中使用 forwardRef()。这让 NestJS 知道,虽然 AServiceBService 彼此依赖,但它们的实例化过程是可以暂时不完全的,直到对方的服务被完全初始化。

这样,通过 forwardRef() 的使用,我们就可以避免循环依赖的问题,并让 NestJS 正确地解析所有的依赖关系。

image.png

2. 使用接口或抽象类

有时,可以通过引入接口或抽象类来避免直接依赖具体实现。这种方式可以解耦模块之间的关系,从而避免循环依赖。

// common/interfaces.ts
export interface IDataService {
  getData(): string;
}

// data.service.ts
import { Injectable } from '@nestjs/common';
import { IDataService } from './common/interfaces';

@Injectable()
export class DataService implements IDataService {
  getData(): string {
    return 'data';
  }
}

// some.service.ts
import { Injectable } from '@nestjs/common';
import { IDataService } from './common/interfaces';

@Injectable()
export class SomeService {
  constructor(private readonly dataService: IDataService) {}

  fetchData(): string {
    return this.dataService.getData();
  }
}

在这个例子中,SomeService 依赖于 IDataService 接口,而不是具体的 DataService 实现。这就减少了直接依赖的紧耦合关系。

结论

循环依赖是开发过程中常见的问题,特别是在大型项目中。通过使用 forwardRef()、接口和抽象类等方法,我们可以有效地避免和解决循环依赖问题。最重要的是,保持代码的模块化解耦性,尽量减少模块之间的直接依赖,从而提升应用的可维护性和可扩展性。