NestJS / 38 минут
Dependency Injection
Заменяйте конкретные реализации через токены провайдеров.
- Заменяйте конкретные реализации через токены провайдеров.
- Применить тему в задаче «Dependency Injection» и объяснить граничные случаи.
Около 8 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Объекты и их зависимости
Сервису цен полезны часы, репозиторий товаров и клиент оплаты. Если создавать их внутри каждого метода через new, сценарий будет одновременно знать правила покупки, настройки соединения и способ создания клиента. Подменить платёжный сервис в тесте окажется трудно. Dependency Injection передаёт готовые зависимости снаружи, оставляя классу его основную работу.
В Nest контейнер строит граф провайдеров. Для каждого узла известны токен, способ создания и зависимости конструктора. Контроллер просит сервис, сервис просит репозиторий, контейнер разрешает цепочку. Декоратор Injectable сообщает framework о классе, но регистрация в providers модуля всё равно нужна. Контейнер не ищет произвольные файлы на диске по совпадению имени.
@Injectable()
class PriceService {
total(price: number, count: number) {
return price * count;
}
}
@Module({ providers: [PriceService], exports: [PriceService] })
class PricesModule {}Токен существует во время исполнения
Токен — ключ, по которому контейнер находит провайдер. Класс может быть таким ключом, поскольку JavaScript сохраняет конструктор после компиляции. Interface описывает контракт только для TypeScript и исчезает. Если параметр имеет интерфейсный тип, задайте runtime-токен явно через Inject. Полезно держать токен и контракт рядом и экспортировать их из одного файла.
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 даёт второе имя уже зарегистрированному провайдеру и сохраняет его экземпляр.
@Module({
providers: [
{ provide: CLOCK, useValue: { now: () => Date.now() } },
ExpiryService,
{ provide: 'EXPIRY', useExisting: ExpiryService },
],
})
class AppModule {}Два useClass для одного класса могут создать два самостоятельных экземпляра. Это существенно для кеша, соединения и состояния. Если нужна именно ссылка на существующий объект, выбирайте useExisting. Значение useValue не получает автоматической проверки формы по интерфейсу потребителя: типизируйте его при объявлении, а внешние данные проверяйте отдельно.
Фабрики и готовность
Фабрика удобна, когда создание клиента зависит от проверенной конфигурации. Порядок массива inject соответствует параметрам функции. Асинхронная фабрика может вернуть Promise; контейнер дождётся результата перед созданием зависимого провайдера. Не запускайте подключение без ожидания, иначе приложение начнёт принимать запросы раньше готовности инфраструктуры.
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 тестовым провайдером и убедитесь, что фабрика не читает скрытую глобальную переменную. Так одинаковый код сценария сможет работать с реальной инфраструктурой и контролируемой зависимостью в тесте.