Provider va Dependency Injection
Agar NestJS'ning bitta eng muhim g'oyasini tanlash kerak bo'lsa, bu Dependency Injection (bog'liqliklarni injektsiya qilish, qisqacha DI) bo'lardi. Bu tushuncha boshida sehrli tuyulishi mumkin, lekin aslida u juda oddiy va kuchli. Ushbu darsda DI qanday ishlashini, provayderlar nima ekanligini va service'larni modullar orasida qanday ulashishni to'liq o'rganamiz.
Muammo: bog'liqliklarni qo'lda boshqarish
Avval DI nima uchun kerakligini tushunamiz. Faraz qilaylik, UsersService ma'lumotlar bazasi bilan ishlash uchun DatabaseService'ga bog'liq. DI'siz biz uni qo'lda yaratamiz:
// DI SIZ (yomon usul):
class UsersService {
private db: DatabaseService;
constructor() {
// Bog'liqlikni o'zimiz yaratamiz โ bu qattiq bog'lanish:
this.db = new DatabaseService('localhost', 5432, 'parol');
}
}
Bu yondashuvning muammolari:
UsersServiceDatabaseService'ni qanday yaratishni bilishi kerak (host, port, parol) โ bu ortiqcha bog'liqlik;- Testlashda haqiqiy bazani soxta (mock) bilan almashtirib bo'lmaydi;
- Agar 10 ta service
DatabaseService'ni ishlatsa, 10 ta alohida nusxa yaratiladi โ bu isrof; - Bir joyni o'zgartirish uchun barcha service'larni qayta yozish kerak bo'ladi.
Yechim: Dependency Injection
DI g'oyasi juda oddiy: obyekt o'ziga kerakli bog'liqliklarni o'zi yaratmaydi, balki ularni tashqaridan qabul qiladi. Kim yaratadi? NestJS'ning maxsus tizimi โ IoC konteyner (Inversion of Control โ nazoratni teskari qilish).
DI bilan yuqoridagi kod shunday bo'ladi. E'tibor bering, UsersService endi DatabaseService'ni qanday yaratishni bilmaydi โ u shunchaki so'raydi:
@Injectable()
class UsersService {
// Bog'liqlikni tashqaridan qabul qilamiz, o'zimiz yaratmaymiz:
constructor(private readonly db: DatabaseService) {}
// NestJS db'ni avtomatik topib, shu yerga uzatadi
}
Konstruktor injektsiyasi qanday ishlaydi?
NestJS'da eng keng tarqalgan usul โ konstruktor injektsiyasi. Siz service'ni konstruktor argumenti sifatida e'lon qilasiz, NestJS uning tipini ko'radi va tegishli nusxani avtomatik uzatadi. Jarayon quyidagicha:
- Siz konstruktorda
private readonly db: DatabaseServiceyozasiz; - NestJS TypeScript metama'lumotlari orqali
dbning tipiDatabaseServiceekanligini biladi; - IoC konteyner o'z ichida
DatabaseServicenusxasi bor-yo'qligini tekshiradi; - Agar bor bo'lsa, o'shani uzatadi; yo'q bo'lsa โ yangi yaratib, keyingi safar uchun eslab qoladi.
@Injectable()
export class UsersService {
constructor(
private readonly db: DatabaseService,
private readonly logger: LoggerService,
) {}
// Ikkala bog'liqlik ham avtomatik injektsiya qilinadi
async findAll() {
this.logger.log('Foydalanuvchilar so'raldi');
return this.db.query('SELECT * FROM users');
}
}
private readonly yozuvi TypeScript'ning qulay xususiyati: u argumentni avtomatik ravishda klass xossasiga aylantiradi. Ya'ni this.db = db deb qo'lda yozish shart emas. Bu NestJS'da konstruktor injektsiyasi juda ixcham ko'rinishining sababi.Provider nima?
Provider โ bu NestJS injektsiya qila oladigan har qanday narsa. Ko'pincha bu @Injectable service, lekin u qiymat, funksiya yoki tashqi kutubxona ham bo'lishi mumkin. Provider modulning providers massivida ro'yxatdan o'tishi kerak:
@Module({
providers: [UsersService, DatabaseService, LoggerService],
})
export class UsersModule {}
Standart holatda har bir provider singleton โ ya'ni butun ilova bo'ylab uning faqat bitta nusxasi mavjud bo'ladi. DatabaseService'ni 100 ta joyda ishlatsangiz ham, xotirada bitta nusxa bo'ladi va u barcha service'lar orasida ulashiladi. Bu resurslarni tejaydi.
Maxsus provayderlar (custom providers)
Ba'zan siz oddiy klassni emas, balki maxsus qiymat yoki fabrikani injektsiya qilmoqchi bo'lasiz. NestJS buning uchun maxsus provayder sintaksisini beradi. Masalan, konfiguratsiya qiymatini token orqali berish:
@Module({
providers: [
// Oddiy service:
UsersService,
// Qiymat provayderi (masalan, konfiguratsiya):
{
provide: 'API_KALIT',
useValue: 'maxfiy-kalit-12345',
},
// Fabrika provayderi (dinamik qiymat):
{
provide: 'DB_ULANISH',
useFactory: () => {
return new DatabaseService('localhost', 5432);
},
},
],
})
export class UsersModule {}
Token orqali berilgan provayderni olish uchun @Inject dekoratori ishlatiladi (chunki string token'ni TypeScript tipi orqali aniqlab bo'lmaydi):
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class UsersService {
constructor(
@Inject('API_KALIT') private readonly apiKalit: string,
) {}
}
Modullararo import va export
Endi eng muhim mavzuga o'tamiz: bir modulning service'ini boshqa modulda qanday ishlatish mumkin? Standart holatda modulning provayderlari faqat o'sha modul ichida ko'rinadi. Ularni tashqariga chiqarish uchun ikki bosqich bor.
1-bosqich: service'ni beruvchi modulda uni exports massiviga qo'shamiz. Masalan, DatabaseModule o'z DatabaseService'ini eksport qiladi:
// src/database/database.module.ts
import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';
@Module({
providers: [DatabaseService],
exports: [DatabaseService], // Boshqa modullar uchun ochamiz
})
export class DatabaseModule {}
2-bosqich: service'ni ishlatmoqchi bo'lgan modulda DatabaseModule'ni imports'ga qo'shamiz:
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { DatabaseModule } from '../database/database.module';
@Module({
imports: [DatabaseModule], // DatabaseService'ni olish uchun
providers: [UsersService],
})
export class UsersModule {}
Endi UsersService ichida DatabaseService'ni bemalol injektsiya qilishimiz mumkin. NestJS uni DatabaseModule'dan topadi:
@Injectable()
export class UsersService {
// DatabaseService boshqa moduldan keladi, lekin bemalol ishlaydi:
constructor(private readonly db: DatabaseService) {}
}
exports'ga qo'shmasangiz, uni boshqa modulda ishlatib bo'lmaydi โ NestJS "Nest can't resolve dependencies" xatosini beradi. Ikki qoidani yodda tuting: beruvchi modul exports qilishi, oluvchi modul esa uni imports qilishi shart.Global modullar
Ba'zi modullar (masalan, konfiguratsiya yoki logger) deyarli hamma joyda kerak bo'ladi. Ularni har bir modulga alohida import qilish zerikarli. Bunday holatda modulni @Global() deb belgilash mumkin โ shunda uni faqat bir marta import qilib, hamma joyda ishlatish mumkin:
import { Module, Global } from '@nestjs/common';
import { ConfigService } from './config.service';
@Global() // Bu modul global bo'ladi
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
@Global()'ni ehtiyotkorlik bilan ishlating. Hamma narsani global qilish modullararo bog'liqliklarni yashirin qiladi va kodni tushunish qiyinlashadi. Uni faqat haqiqatan ham hamma joyda kerak bo'ladigan (config, logger) modullar uchun ishlating.Xulosa
- Dependency Injection โ obyekt bog'liqliklarni o'zi yaratmaydi, tashqaridan qabul qiladi;
- IoC konteyner bog'liqliklarni yaratib, konstruktorga avtomatik uzatadi;
- Konstruktor injektsiyasi โ
constructor(private readonly x: XService) {}eng keng tarqalgan usul; - Provider โ injektsiya qilinadigan narsa, odatda
@Injectableservice,providers'da ro'yxatdan o'tadi; - Provayderlar standart holatda singleton โ butun ilovada bitta nusxa;
- Service'ni boshqa modulda ishlatish uchun: beruvchi modul
exports, oluvchi modulimportsqiladi; @Global()modulni hamma joyda ochadi, lekin uni ehtiyotkorlik bilan ishlatish kerak.