Пошук уроків, статей та іншого контенту
Зрозумієте принцип інʼєкції залежностей, роль провайдерів і спосіб їхнього отримання в конструкторах класів.
Інʼєкція залежностей — це спосіб передати обʼєкту всі залежності, які йому потрібні для роботи, замість того щоб створювати їх усередині самого обʼєкта.
Наприклад, сервіс замовлень може залежати від сервісу користувачів:
class OrdersService {
private usersService = new UsersService();
// ...
}Такий підхід створює сильну залежність між класами. OrdersService сам вирішує, як створювати UsersService, тому його складніше:
тестувати;
замінювати іншим обʼєктом;
конфігурувати;
повторно використовувати.
З інʼєкцією залежностей клас лише оголошує, що йому потрібен UsersService:
class OrdersService {
constructor(private readonly usersService: UsersService) {}
}Створення та передавання залежності бере на себе зовнішній механізм — у NestJS це контейнер залежностей.
NestJS має контейнер залежностей, який:
знає, які класи зареєстровані як провайдери;
створює екземпляри цих класів;
знаходить залежності їхніх конструкторів;
передає залежності під час створення обʼєкта;
повторно використовує екземпляри відповідно до їхнього scope.
Зазвичай розробник не створює сервіс через new. Замість цього NestJS створює його автоматично та передає в конструктор потрібні залежності.
@Injectable()
export class UsersService {
findById(id: number) {
return { id, name: 'Olena' };
}
}
@Injectable()
export class OrdersService {
constructor(
private readonly usersService: UsersService,
) {}
getOrderOwner(orderId: number) {
const user = this.usersService.findById(1);
return {
orderId,
user,
};
}
}У цьому прикладі:
UsersService є залежністю;
OrdersService залежить від UsersService;
NestJS передасть екземпляр UsersService у конструктор OrdersService.
Провайдер — це обʼєкт або клас, який NestJS може створити та передати іншим класам як залежність.
Найпоширеніший провайдер — клас із декоратором @Injectable():
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
findAll() {
return [
{ id: 1, name: 'Olena' },
{ id: 2, name: 'Taras' },
];
}
}Декоратор @Injectable() повідомляє NestJS, що клас може використовуватися системою інʼєкції залежностей. Сам декоратор не створює екземпляр у момент оголошення класу. Екземпляр створюється контейнером, коли він потрібен.
Провайдер потрібно зареєструвати в модулі:
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
})
export class UsersModule {}Масив providers повідомляє NestJS, що UsersService доступний як провайдер у межах цього модуля.
Залежність отримують через параметр конструктора:
import { Injectable } from '@nestjs/common';
import { UsersService } from './users.service';
@Injectable()
export class OrdersService {
constructor(
private readonly usersService: UsersService,
) {}
getOrder(orderId: number) {
const owner = this.usersService.findById(1);
return {
id: orderId,
owner,
};
}
}Параметр конструктора одночасно:
оголошує залежність;
створює приватне поле класу завдяки модифікаторам параметрів;
дає NestJS інформацію, який провайдер потрібно знайти.
Запис:
constructor(
private readonly usersService: UsersService,
) {}еквівалентний більш розгорнутому варіанту:
private readonly usersService: UsersService;
constructor(usersService: UsersService) {
this.usersService = usersService;
}private readonly часто використовують для залежностей, тому що:
private обмежує доступ до поля межами класу;
readonly не дозволяє переприсвоїти залежність після створення обʼєкта.
Провайдер доступний лише в тих модулях, де він зареєстрований або експортований і підключений через імпорт.
Розглянемо повний приклад із двома модулями.
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
findById(id: number) {
return {
id,
name: 'Olena',
};
}
}// users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}providers робить UsersService провайдером цього модуля.
exports дозволяє іншим модулям отримувати цей провайдер через імпорт UsersModule.
// orders.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class OrdersService {
constructor(
private readonly usersService: UsersService,
) {}
findById(id: number) {
const owner = this.usersService.findById(1);
return {
id,
owner,
};
}
}// orders.module.ts
import { Module } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}Тут відбувається така послідовність:
UsersModule реєструє UsersService.
UsersModule експортує UsersService.
OrdersModule імпортує UsersModule.
OrdersService вказує UsersService у конструкторі.
NestJS знаходить провайдер і передає його в OrdersService.
Контролери також можуть отримувати провайдери через конструктор. Це типовий спосіб викликати бізнес-логіку сервісу з HTTP-обробника.
import { Controller, Get, Param } from '@nestjs/common';
import { OrdersService } from './orders.service';
@Controller('orders')
export class OrdersController {
constructor(
private readonly ordersService: OrdersService,
) {}
@Get(':id')
findById(@Param('id') id: string) {
return this.ordersService.findById(Number(id));
}
}Контролер не створює OrdersService вручну:
// Не потрібно робити так:
const ordersService = new OrdersService(/* залежності */);NestJS створить контролер і передасть йому вже налаштований OrdersService.
Щоб приклад працював, контролер потрібно зареєструвати в модулі:
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}Нижче наведено мінімальний приклад структури NestJS-застосунку:
// users/users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
findById(id: number) {
return {
id,
name: 'Olena',
};
}
}// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}// orders/orders.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class OrdersService {
constructor(
private readonly usersService: UsersService,
) {}
findById(id: number) {
return {
id,
owner: this.usersService.findById(1),
};
}
}// orders/orders.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { OrdersService } from './orders.service';
@Controller('orders')
export class OrdersController {
constructor(
private readonly ordersService: OrdersService,
) {}
@Get(':id')
findById(@Param('id') id: string) {
return this.ordersService.findById(Number(id));
}
}// orders/orders.module.ts
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [UsersModule],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}// app.module.ts
import { Module } from '@nestjs/common';
import { OrdersModule } from './orders/orders.module';
@Module({
imports: [OrdersModule],
})
export class AppModule {}Після запуску застосунку запит до GET /orders/10 викличе метод контролера, який використає OrdersService. OrdersService своєю чергою використає автоматично переданий UsersService.
За замовчуванням провайдер має singleton scope. Це означає, що NestJS створює один екземпляр провайдера в межах його модуля та повторно використовує його під час наступних інʼєкцій.
Наприклад, якщо UsersService інʼєктований у кілька інших класів, NestJS зазвичай передає їм той самий екземпляр цього провайдера.
Це дозволяє:
не створювати сервіс щоразу заново;
зберігати спільний стан провайдера, якщо це справді потрібно;
централізовано керувати створенням залежностей.
Тому провайдери зазвичай мають бути безпечними для повторного використання. Бізнес-операції краще зберігати в методах сервісу, а не покладатися на змінний стан його полів.
Клас не відповідає за створення своїх залежностей:
@Injectable()
export class OrdersService {
constructor(
private readonly usersService: UsersService,
) {}
}Йому не потрібно знати, як саме створюється UsersService.
Під час тестування можна передати замість справжнього сервісу обʼєкт із потрібною поведінкою:
const fakeUsersService = {
findById: () => ({
id: 1,
name: 'Test user',
}),
};Тестований клас отримує залежність зовні, тому не мусить підключати всю її реалізацію.
Сервіс зосереджується на власній логіці, а контейнер NestJS відповідає за створення та звʼязування обʼєктів.
providersЯкщо клас використовується як залежність, але не зареєстрований у модулі, NestJS не зможе його знайти.
@Module({
providers: [OrdersService],
})
export class OrdersModule {}Якщо OrdersService залежить від UsersService, одного імпорту файлу недостатньо. UsersService має бути доступним через модулі.
@Injectable()Для звичайного класу-провайдера потрібно додати декоратор:
@Injectable()
export class UsersService {}Без нього NestJS не матиме коректної інформації для роботи з класом як із провайдером.
Якщо залежність зареєстрована в іншому модулі, цей модуль має її експортувати:
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}Модуль, у якому потрібен провайдер, має імпортувати модуль, що його експортує:
@Module({
imports: [UsersModule],
providers: [OrdersService],
})
export class OrdersModule {}new@Injectable()
export class OrdersService {
private readonly usersService = new UsersService();
}Це обходить контейнер NestJS і ускладнює заміну залежності та тестування. Якщо клас є залежністю, отримуйте його через конструктор.
TypeScript-інтерфейси видаляються під час компіляції. NestJS не може використати такий інтерфейс як runtime-токен:
interface Logger {
log(message: string): void;
}
// Такий тип не дає NestJS runtime-ідентифікатор залежності
constructor(private readonly logger: Logger) {}У такому випадку використовують конкретний клас або явний токен із @Inject(). Для звичайних класів достатньо типу класу, наприклад UsersService.
Інʼєкція залежностей передає обʼєкту потрібні залежності ззовні.
NestJS використовує контейнер залежностей для створення та звʼязування обʼєктів.
Провайдер — це клас або інший обʼєкт, який контейнер може надати як залежність.
Класи-провайдери зазвичай позначають декоратором @Injectable().
Провайдери реєструють у providers модуля.
Для отримання залежності її вказують у конструкторі.
Щоб провайдер був доступний іншому модулю, його потрібно експортувати через exports, а модуль-постачальник — імпортувати через imports.
Створення залежностей через new усередині класів зазвичай обходить механізм DI NestJS і робить код менш гнучким.