NestJS / 38 минут

Dependency Injection

Заменяйте конкретные реализации через токены провайдеров.

В процессе Теория: впереди Практика: впереди Тест: впереди
03 / 14
После этой главы
  • Заменяйте конкретные реализации через токены провайдеров.
  • Применить тему в задаче «Dependency Injection» и объяснить граничные случаи.

Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Объекты и их зависимости

Сервису цен полезны часы, репозиторий товаров и клиент оплаты. Если создавать их внутри каждого метода через new, сценарий будет одновременно знать правила покупки, настройки соединения и способ создания клиента. Подменить платёжный сервис в тесте окажется трудно. Dependency Injection передаёт готовые зависимости снаружи, оставляя классу его основную работу.

В Nest контейнер строит граф провайдеров. Для каждого узла известны токен, способ создания и зависимости конструктора. Контроллер просит сервис, сервис просит репозиторий, контейнер разрешает цепочку. Декоратор Injectable сообщает framework о классе, но регистрация в providers модуля всё равно нужна. Контейнер не ищет произвольные файлы на диске по совпадению имени.

TypeScript
@Injectable()
class PriceService {
  total(price: number, count: number) {
    return price * count;
  }
}
@Module({ providers: [PriceService], exports: [PriceService] })
class PricesModule {}

Токен существует во время исполнения

Токен — ключ, по которому контейнер находит провайдер. Класс может быть таким ключом, поскольку JavaScript сохраняет конструктор после компиляции. Interface описывает контракт только для TypeScript и исчезает. Если параметр имеет интерфейсный тип, задайте runtime-токен явно через Inject. Полезно держать токен и контракт рядом и экспортировать их из одного файла.

TypeScript
const CLOCK = Symbol('CLOCK');
interface Clock { now(): number }
@Injectable()
class ExpiryService {
  constructor(@Inject(CLOCK) private readonly clock: Clock) {}
  deadline(ttl: number) { return this.clock.now() + ttl; }
}

Не создавайте новый Symbol в файле потребителя. Даже одинаковое описание не делает два Symbol равными. Строковые токены тоже разрешены, но требуют дисциплины имён. При ошибке разрешения проверьте именно идентичность токена, доступность модуля и exports, а не только написание имени класса.

Четыре способа регистрации

Короткая запись providers: [PriceService] означает регистрацию класса под собственным токеном. useClass позволяет назначить реализацию другому токену. useValue передаёт уже готовый объект: конфигурацию, неизменяемые настройки или тестовую зависимость. useFactory создаёт значение функцией и получает нужные зависимости через массив inject. useExisting даёт второе имя уже зарегистрированному провайдеру и сохраняет его экземпляр.

TypeScript
@Module({
  providers: [
    { provide: CLOCK, useValue: { now: () => Date.now() } },
    ExpiryService,
    { provide: 'EXPIRY', useExisting: ExpiryService },
  ],
})
class AppModule {}

Два useClass для одного класса могут создать два самостоятельных экземпляра. Это существенно для кеша, соединения и состояния. Если нужна именно ссылка на существующий объект, выбирайте useExisting. Значение useValue не получает автоматической проверки формы по интерфейсу потребителя: типизируйте его при объявлении, а внешние данные проверяйте отдельно.

Фабрики и готовность

Фабрика удобна, когда создание клиента зависит от проверенной конфигурации. Порядок массива inject соответствует параметрам функции. Асинхронная фабрика может вернуть Promise; контейнер дождётся результата перед созданием зависимого провайдера. Не запускайте подключение без ожидания, иначе приложение начнёт принимать запросы раньше готовности инфраструктуры.

TypeScript
const doubleProvider = {
  provide: 'DOUBLE',
  inject: ['VALUE'],
  useFactory: (value: number) => value * 2,
};

Ошибки запуска должны объяснять проблему оператору, не раскрывая credentials в логах. Восстановимые сбои во время запроса и невозможность первоначальной настройки — разные ситуации. Регистрация фабрики не заменяет обработку таймаутов внешнего клиента и освобождение его ресурсов при завершении.

Время жизни провайдера

По умолчанию провайдер имеет singleton scope в контексте приложения. Это удобно для сервиса без пользовательского состояния и для пула БД. Не кладите текущего пользователя в поле такого сервиса: параллельные запросы увидят общий объект. Передавайте пользовательские значения аргументами сценария.

Request scope создаёт зависимость для запроса, transient — для соответствующего потребителя. Эти режимы влияют на граф и стоимость создания объектов. Они не нужны просто ради «чистой архитектуры». Начинайте с обычных сервисов и явных параметров; меняйте scope только при конкретном требовании. Два отдельных application context имеют разные контейнеры, даже если используют один класс модуля.

Практика фабричного провайдера

В задаче зарегистрируйте переданное число под VALUE, а DOUBLE создайте через useFactory с inject: [VALUE]. Получите результат из настоящего application context и закройте контекст в finally. Не возвращайте умножение напрямую из solve: цель упражнения — проверить граф зависимостей и выбранный токен.

Пройдите вручную путь нуля и отрицательного числа. Число ноль является полноценным значением, поэтому проверка через if (!value) здесь ошибочна. После решения замените VALUE тестовым провайдером и убедитесь, что фабрика не читает скрытую глобальную переменную. Так одинаковый код сценария сможет работать с реальной инфраструктурой и контролируемой зависимостью в тесте.

Загружаем редактор…
Следующая темаКонтроллеры и сервисы

Музыка: Reverie — Scott Buckley · CC BY 4.0