Правильный способ избежать циклических зависимостей - NestJS - PullRequest
1 голос
/ 28 мая 2020

Допустим, у меня есть 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.1 и A.2 выше заключается в том, что StudentService имеет доступ к StudentRepository, а LessonService имеет доступ к LessonRepository, что, как я полагаю, соответствует разделению функций solid.

Однако, похоже, анти-паттерн, что StudentModule должен импортировать LessonModule, а LessonModule должен импортировать StudentModule. Это можно исправить с помощью метода forwardRef, но в Nest JS Documentation упоминается, что этого шаблона следует по возможности избегать:

Хотя следует избегать циклических зависимостей, где возможно, вы не всегда можете это сделать. (это один из тех случаев?)

Кажется, это должна быть общая проблема при использовании DI, но я изо всех сил пытаюсь получить окончательный ответ относительно того, что доступны варианты, которые могут устранить эту ситуацию, или если я наткнулся на ситуацию, в которой это неизбежно.

Конечная цель для меня - написать два запроса GraphQL ниже:

query {
  students {
    firstName
    lessons {
      name
    }
  }
}

query {
  lessons {
    name
    students {
      firstName
    }
  }
}

1 Ответ

2 голосов
/ 28 мая 2020

Вероятно, самый простой способ - полностью отбросить зависимости и вместо этого ввести третий модуль, который зависит от двух других. В вашем случае вы можете объединить свои два преобразователя в один StudentLessonResolver, который находится в собственном модуле, скажем, ResolverModule:

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

Итак, StudentModule и LessonModule теперь полностью независимы, а ResolverModule зависит от них обоих. Больше нет цикла :)

Если по какой-то причине вам нужно иметь два преобразователя и заставить их обновлять друг друга, вы можете использовать события или обратные вызовы для публикации sh изменений. Затем вы снова вводите третий модуль, который прослушивает эти события и обновляет другой модуль.


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);
});

Опять же, вы прерываете цикл, потому что StudentModule и LessonModule не знают друг о друге, в то время как ваши зарегистрированные обратные вызовы гарантируют, что вызов любого преобразователя приведет к обновлению обеих служб.

Если вы используете реактивную библиотеку, такую ​​как Rx JS, вместо управления обратными вызовами вручную вы должны использовать Subject<AssignStudentsToLessonInput>, для которого преобразователи publi sh и новый представленный модуль подписывается.

Обновление

Как было предложено OP, есть и другие альтернативы, такие как внедрение обоих репозиториев в обе службы . Но если каждый модуль содержит и репозиторий, и службу, то есть если вы импортируете LessonRepository и LessonService из LessonModule, это не сработает, поскольку у вас все равно будет зависимость cycli c от уровня модуля. Но если между учениками и уроками действительно существует тесная связь, вы также можете объединить два модуля в один, и не будет проблем.

Аналогичным вариантом было бы изменение единственного преобразователя первое решение для сервиса, который напрямую использует репозитории. Будет ли это хорошим вариантом, зависит от сложности управления магазинами. В долгосрочной перспективе вам, вероятно, будет лучше воспользоваться службой. решения, studentService.assignLessonToStudents и lessonService.assignStudentsToLesson фактически делают одно и то же, поэтому неясно, какой из них следует использовать.

...