Провайдеры (providers) — ключевая концепция фреймворка. Базовые классы, такие как сервисы (services), фабрики (factories) и репозитории (repositories), могут быть провайдерами. Это одна из основных абстракций фреймворка Nest. Особенность провайдеров — возможность внедрения зависимостей. Это позволяет создавать сложные связи между объектами. Nest отвечает за создание экземпляров и передачу зависимостей.
providers.jpg
Провайдеры
Провайдер — это обычный класс (хотя не всегда так), который, например, может быть сервисом. Чтобы класс стал провайдером, его нужно пометить специальным декоратором и зарегистрировать.
Провайдеры на практике
Многие базовые классы в Nest могут быть провайдерами. Рассмотрим это на простом примере. Предположим, мы создаём приложение для доски объявлений, где пользователи могут размещать объявления о продаже ненужных вещей, а другие могут комментировать эти объявления. Пример архитектуры приложения представлен на следующем рисунке.
providers-1.jpg
При разработке приложения логично разделить его на три модуля: объявления (offers), пользователи (users) и комментарии (comments). Каждый модуль будет иметь свой сервис для бизнес-логики и работы с базой данных.
Представим задачу: удаление объявления должно автоматически удалять все связанные комментарии. Например, к объявлению с идентификатором 12 пользователи оставили 100 комментариев. Эти комментарии хранятся в отдельной коллекции и управляются отдельным сервисом. Нам нужно реализовать метод в контроллере OfferController, который будет удалять объявление и его комментарии.
Одно из решений — использовать два сервиса (для объявлений и комментариев) в контроллере OfferController. Сервис комментариев предоставляет метод для удаления всех комментариев к определённому объявлению, а сервис объявлений — метод для удаления самого объявления. Таким образом, в контроллере OfferController понадобятся оба сервиса для выполнения задачи.
providers-2.jpg
Схематично взаимодействие контроллера и сервисов показано на следующем рисунке. Контроллеру нужны два сервиса: OfferService и CommentService. Их можно подключить напрямую или внедрить как зависимости. Здесь на помощь придут провайдеры. Многие базовые классы могут быть провайдерами, и OfferService и CommentService отлично подходят для этой роли.
Внедрение зависимостей
Nest использует свой IoC-контейнер и паттерн внедрения зависимостей (DI). Фреймворк упрощает использование DI, предоставляя разработчикам более простой API. Важно следовать подходам и ограничениям, установленным фреймворком.
Зависимость — это объект, от которого зависит другой объект. Например, контроллеру нужны классы-сервисы для работы.
Теперь познакомимся с внедрением зависимостей и созданием провайдеров. Для создания провайдера нужно выполнить несколько шагов:
Применить декоратор @Injectable() из модуля @nestjs/common к классу.
Этот декоратор помечает класс, экземпляры которого будут внедряться как зависимости. Он принимает объект настроек, который опционален. Позже мы разберёмся с ним, а сейчас рассмотрим пример применения декоратора.
Ниже приведён фрагмент кода с базовой реализацией сервисов для нашего приложения:
@Injectable()
export class OfferService {
private offers: Offer[];
public async create() { /* Не реализовано */ }
public async deleteById(offerId: number) { /* Не реализовано */ }
}
@Injectable()
export class CommentService {
private comments: Comment[];
public async create() { /* Не реализовано */ }
public async deleteComments(offerId: number) { /* Не реализовано */ }
}
Когда класс помечен декоратором @Injectable, это означает, что им может управлять встроенный IoC-контейнер Nest. Он будет создавать и хранить экземпляры класса, избавляя нас от необходимости делать это вручную.
Внедрение зависимости в конструктор
Следующий шаг — внедрить провайдер как зависимость. Здесь проявляется «магия» Nest. Чтобы внедрить зависимость через параметр конструктора, не нужно делать ничего особенного. Просто укажите тип параметра, а Nest сам разрешит зависимость, создаст новый или переиспользует существующий экземпляр и передаст его в параметр. В коде контроллера это может выглядеть так:
@Controller('offer')
export class OfferController {
constructor(
// Внедряем зависимости
private readonly commentService: CommentService,
private readonly offerService: OfferService,
) {}
@Delete('/:offerId')
public delete() {}
}
Обратите внимание на параметры commentService и offerService в конструкторе. Дополнительные декораторы для внедрения не нужны — Nest сам разрешит зависимости по типу. Главное, чтобы нужный провайдер был доступен в модуле.
Регистрация провайдера
Регистрация провайдера происходит в секции providers декоратора @Module. Это массив, в котором перечисляются все провайдеры модуля. На этом этапе класс регистрируется в IoC-контейнере. Для регистрации достаточно указать имена классов, без выделения токена (более сложные случаи рассмотрим позже):
@Module({
// Регистрируем в IoC-контейнере
providers: [OfferService],
controllers: [OfferController],
// Импортируем модуль
// Позже разберём причины импорта модуля
imports: [CommentModule]
})
export class OfferModule {}
Обратите внимание на ситуации, когда модулю нужен провайдер из другого модуля. Как мы уже упоминали, «провайдер должен быть доступен в области видимости модуля, в котором используется». Провайдеры одного модуля инкапсулированы, что означает, что один модуль не может получить доступ к провайдерам другого. Например, модулю OfferModule нужен провайдер из CommentModule.
IoC-контейнер можно представить как коробку, в которой аккуратно хранятся классы для внедрения зависимостей. Он отвечает за создание экземпляров классов и разрешение вложенных зависимостей.
Важно не забыть экспортировать провайдеры. Это делается через декоратор @Module. Рассмотрим это на примере модуля CommentModule:
@Module({
controllers: [CommentController],
providers: [CommentService],
// Экспортируем провайдер
exports: [CommentService],
})
export class CommentModule {}
Обратите внимание на секцию exports. Здесь перечислены сервисы (провайдеры), доступные другим модулям при импорте CommentModule. В декораторе мы также определяем секцию imports, где импортируем CommentModule.
Важно запомнить: модуль экспортирует провайдеры. Если другой модуль хочет использовать провайдер, ему нужно просто импортировать модуль с этими провайдерами.
Вернёмся к нашему примеру: модуль CommentModule экспортирует провайдер CommentService, который нужен в контроллере модуля OfferModule. Поэтому модуль объявлений импортирует CommentModule.
На этом процесс работы с провайдерами завершён. Nest автоматически внедрит необходимые провайдеры в зависимости. Вкратце: используем декоратор @Injectable, следим за доступностью провайдера и внедряем зависимость через параметры конструктора.
photo_2024-11-09_14-46-10.jpg
Если следовать интерфейсу ClassProvider, то запись должна содержать два свойства: useClass (ссылка на класс) и provide (токен). По этому токену механизм DI сможет получить нужные данные, например, экземпляр класса. Давайте применим это к нашему примеру:
@Module({
controllers: [UsersController],
providers: [
{
provide: UserService,
useClass: UserService,
},
],
exports: [UserService],
})
export class UsersModule {}
Посмотрев на полную запись, становится яснее, что для регистрации класса в IoC-контейнере нужен токен. В нашем примере мы используем UserService в качестве токена, связывая его с классомUserService. Когда Nest будет искать зависимость, он будет искать её именно по этому токену.
Разрешение зависимостей не так просто, как кажется. Nest старается упростить эту задачу для разработчиков, строя граф зависимостей на этапе загрузки приложения. Этот граф содержит информацию обо всех зависимостях и гарантирует правильный порядок их разрешения.
Заключение
Провайдеры — это абстракция, которая позволяет внедрять классы как зависимости. Многие стандартные классы могут быть провайдерами. Nest упрощает внедрение: достаточно пометить класс декоратором @Injectable и зарегистрировать его в секции providers декоратора @Module. Для внедрения через конструктор достаточно указать параметр нужного типа — остальное сделает Nest.