【问题标题】:Avoiding circular dependencies the right way - NestJS以正确的方式避免循环依赖 - NestJS
【发布时间】:2020-09-15 04:16:33
【问题描述】:

假设我有一个 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 ****
  }

在我的StudentResolver中基本上是相反的

上面A.1A.2的区别在于StudentService可以访问StudentRepositoryLessonService可以访问LessonRepository - 我相信它坚持关注点的可靠分离。

但是,StudentModule 必须导入 LessonModuleLessonModule 必须导入 StudentModule 似乎是一种反模式。这可以使用forwardRef 方法解决,但在NestJS Documentation 中提到如果可能的话应该避免这种模式:

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

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

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

query {
  students {
    firstName
    lessons {
      name
    }
  }
}

query {
  lessons {
    name
    students {
      firstName
    }
  }
}

【问题讨论】:

    标签: typescript design-patterns dependency-injection graphql nestjs


    【解决方案1】:

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

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

    所以StudentModuleLessonModule 现在是完全独立的,而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);
    });
    

    再次,您打破循环是因为 StudentModuleLessonModule 彼此不了解,而您注册的回调保证调用任一解析器会导致两个服务都被更新。

    如果您使用的是 RxJS 等响应式库,您应该使用 Subject&lt;AssignStudentsToLessonInput&gt;,而不是手动管理回调,解析器发布并订阅新引入的模块。

    更新

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

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

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

    【讨论】:

    • 有趣的方法!我曾想过使用第三个解析器,但不确定这是否是我过度设计并且未能看到更简单的解决方案。我刚刚想到的一个想法是将StudentRepositoryLessonRepository 交替注入StudentServiceLessonService。这样,每个服务都有两个存储库,并且可以相应地更新它们。我对这种方法的唯一抱怨是它似乎引入了重复的业务逻辑。好奇你的想法是什么?
    • 我刚刚尝试了您的第一种方法,效果很好。感谢您的反馈!
    • 谢谢!我还更新了答案以考虑您的建议。
    猜你喜欢
    • 1970-01-01
    • 2013-02-06
    • 1970-01-01
    • 2011-01-27
    • 2012-02-15
    • 2011-12-24
    • 2021-05-28
    • 2012-08-10
    • 1970-01-01
    相关资源
    最近更新 更多