以正确的方式避免循环依赖 - NestJS

col*_*lby 5 design-patterns dependency-injection typescript graphql nestjs

假设我有一种StudentService方法可以为学生添加课程,另LessonService一种方法是将学生添加到课程中。在我的课程和学生解析器中,我希望能够更新本课程 <---> 学生关系。所以在我的内容中,LessonResolver我有一些类似的东西:

  async assignStudentsToLesson(
    @Args('assignStudentsToLessonInput')
    assignStudentsToLesson: AssignStudentsToLessonInput,
  ) {
    const { lessonId, studentIds } = assignStudentsToLesson;
    await this.studentService.assignLessonToStudents(lessonId, studentIds); **** A.1 ****
    return this.lessonService.assignStudentsToLesson(lessonId, studentIds); **** A.2 ****
  }
Run Code Online (Sandbox Code Playgroud)

和我的基本相反 StudentResolver

上面A.1A.2之间的区别在于StudentService有权访问StudentRepositoryLessonService有权访问LessonRepository- 我相信这遵循了关注点的明确分离。

但是,StudentModule必须导入LessonModuleLessonModule必须导入StudentModule. 这是可以使用该forwardRef方法修复的,但是在NestJS 文档中它提到如果可能的话应该避免这种模式:

虽然应该尽可能避免循环依赖,但你不能总是这样做。(这是其中一种情况吗?)

在使用 DI 时,这似乎应该是一个常见问题,但我正在努力获得关于哪些选项可以消除这种情况的明确答案,或者我是否偶然发现了一种不可避免的情况。

最终目标是让我能够编写以下两个 GraphQL 查询:

query {
  students {
    firstName
    lessons {
      name
    }
  }
}

query {
  lessons {
    name
    students {
      firstName
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

mpe*_*old 5

可能最简单的方法是完全删除依赖项,而是引入依赖于其他两个模块的第三个模块。在您的情况下,您可以将两个解析器合并为一个StudentLessonResolver位于其自己模块中的单个解析器,例如ResolverModule

async assign({ lessonId, studentIds }: AssignStudentsToLessonInput) {
  await this.studentService.assignLessonToStudents(lessonId, studentIds);
  return this.lessonService.assignStudentsToLesson(lessonId, studentIds);
}
Run Code Online (Sandbox Code Playgroud)

SoStudentModuleLessonModule现在完全独立,而ResolverModule依赖于它们两者。没有循环了:)

如果由于某种原因您需要有两个解析器并让它们相互更新,您可以使用事件或回调来发布更改。然后,您将再次引入第三个模块,该模块侦听这些事件并更新另一个模块。


type AssignCallback = (assignStudentsToLesson: AssignStudentsToLessonInput) => Promise<void>;

class LessonResolver {  // and similar for StudentResolver
  private assignCallbacks: AssignCallback[] = [];

  // ... dependencies, constructor etc.

  onAssign(callback: AssignCallback) {
    assignCallbacks.push(callback);
  }

  async assignStudentsToLesson(
    @Args('assignStudentsToLessonInput')
    assignStudentsToLesson: AssignStudentsToLessonInput,
  ) {
    const { lessonId, studentIds } = assignStudentsToLesson;
    await this.lessonService.assignStudentsToLesson(lessonId, studentIds); **** A.2 ****
    for (const cb of assignCallbacks) {
      await cb(assignStudentsToLesson);
    }
  }
}

// In another module
this.lessonResolver.onAssign(({ lessonId, studentIds }) => {
  this.studentService.assignLessonToStudents(lessonId, studentIds);
});
this.studentResolver.onAssign(({ lessonId, studentIds }) => {
  this.lessonService.assignStudentsToLesson(lessonId, studentIds);
});
Run Code Online (Sandbox Code Playgroud)

同样,您打破了循环,因为StudentModule并且LessonModule不了解彼此,而您注册的回调保证调用任一解析器都会导致两个服务都被更新。

如果您使用的是响应式库,例如 RxJS,则不应使用手动管理回调,您应该使用Subject<AssignStudentsToLessonInput>解析器发布和新引入的模块订阅的 a。

更新

正如 OP 所建议的那样,还有其他替代方案,例如将两个存储库注入到两个服务中。但是如果每个模块都包含存储库和服务,即如果您 importLessonRepositoryLessonServicefrom LessonModule,这将不起作用,因为您仍然会在模块级别拥有循环依赖。但是如果学生和课程之间真的有紧密的联系,你也可以将两个模块合并为一个,也没有问题。

一个类似的选择是将第一个解决方案的单个解析器更改为直接使用存储库的服务。这是否是一个好的选择取决于管理商店的复杂性。从长远来看,您最好通过该服务。

我在单一解析器/服务解决方案中看到的一个优点是它提供了一个单一的解决方案来为学生分配课程,而在事件解决方案中, studentService.assignLessonToStudents 和 courseService.assignStudentsToLesson 有效地做完全相同的事情,所以不清楚应该使用哪一个。