Šta je Dependency Injection (DI)?
Dependency Injection (DI) je softverski obrazac dizajna koji omogućava razdvajanje zavisnosti objekata i njihove inicijalizacije. U kontekstu Nest.js, DI omogućava lakše upravljanje zavisnostima, smanjujući međusobnu povezanost klasa i povećavajući modularnost i testabilnost aplikacije. DI je softerski obrazac koji nije vezan samo za nest.js ili node.js već se koristi i u drugim programskim jezicima, pročitajte kako se uopšteno koristi dependency injection-u u članku Šta je “Dependency Injection”?.
DI je najlakše objasniti kroz primer, zamislite da imate klasu koja šalje e-poštu korisnicima. Ta klasa može direktno kreirati instancu objekta koji šalje e-poštu (SMTP servis), ili možete koristiti Dependency Injection (DI) kako biste taj zadatak prepustili eksternom entitetu (IoC kontejneru).
Primer bez Dependency Injection-a (Loša praksa)
U ovome primeru se instanca Smpt servisa kreira u konstruktoru:
class EmailService {
private smtpService: SmtpService;
constructor() {
this.smtpService = new SmtpService(); // Kreiranje zavisnosti unutar klase
}
sendEmail(to: string, message: string): void {
this.smtpService.send(to, message);
}
}
SmtpService je zavisnost (eng. dependency) za EmailService, te kreiranje njene instance u konstruktoru, obezbedjuje tzv. čvrstu povezanost, jer klasa EmailService zavisi od konkretne implementacije SmtpService. Kreiranje sopstvene instance u konstruktoru nas dovodi do više problema:
- Otežana izmena servisa jer bi svaka promena u implementaciji SMTP servisa npr. dodavanje novih zavisnosti u SMTP servis bi zahtevala promene u svakoj klasi gde je instancirana ta klasa,
- Teško testiranje jer nije moguće lako mockovati SmtpService u testovima.
Primer sa Dependency Injection-om (Dobra praksa)
Sada ćemo koristiti DI tako što ćemo premestiti odgovornost za kreiranje zavisnosti iz klase EmailService u eksterni IoC kontejner:
import { Injectable } from '@nestjs/common';
@Injectable()
export class EmailService {
constructor(private readonly smtpService: SmtpService) {} // Zavisnost se injektuje
sendEmail(to: string, message: string): void {
this.smtpService.send(to, message);
}
}
U ovom slučaju, SmtpService će biti kreiran i upravljan od strane IoC kontejnera u Nest.js.
Registracija u modulu
import { Module } from '@nestjs/common';
@Module({
providers: [SmtpService, EmailService], // IoC kontejner registruje provajdere
})
export class AppModule {}
Ključne razlike
- Odgovornost za kreiranje zavisnosti:
- Bez DI: Klasa sama kreira instancu zavisnosti.
- Sa DI: IoC kontejner kreira instancu i pruža je klasi.
- Fleksibilnost: Promena samog SmtpService servisa ili čak zamena za neki drugi servis je jednostavna — sada je dovoljno zameniti provajdera u IoC kontejneru, bez promene koda u EmailService.
@Module({ providers: [ { provide: SmtpService, useClass: AlternativeEmailService, // Zamena implementacije }, EmailService, ], }) export class AppModule {} - Testiranje: Lako se mockuje zavisnost u testovima:
const mockSmtpService = { send: jest.fn() }; const emailService = new EmailService(mockSmtpService as SmtpService);
DI u praksi sa Nest.js
Umesto da klase same kreiraju svoje zavisnosti, DI prenosi odgovornost za kreiranje zavisnosti nekom eksternom entitetu — obično kontejneru za injekciju zavisnosti. DI je praktična implementacija šireg koncepta poznatog kao Inverzija kontrole (Inversion of Control, IoC). IoC preokreće uobičajeni tok programa — umesto da aplikacija kontroliše kako se zavisnosti kreiraju i koriste, taj zadatak preuzima IoC kontejner. U Nest.js, IoC kontejner automatski upravlja zavisnostima i njihovim životnim ciklusom na osnovu definisanih pravila a osnovne komponente koje DI koristi u Nest.js su:
- Provideri: Klase ili objekti koji se mogu injektovati.
- Moduli: Organizuju aplikaciju i definišu koji provajderi su dostupni.
- Dekoratori: Obeležavaju klase, metode ili parametre da bi označili njihove uloge u DI sistemu.
Korak 1: Definisanje provajdera
import { Injectable } from '@nestjs/common';
@Injectable()
export class ExampleService {
getHello(): string {
return 'Hello, Dependency Injection!';
}
}
Dekorator @Injectable() omogućava Nest-u da registruje ovu klasu kao provajdera u IoC kontejneru.
Korak 2: Injektovanje zavisnosti
import { Controller, Get } from '@nestjs/common';
import { ExampleService } from './example.service';
@Controller('example')
export class ExampleController {
constructor(private readonly exampleService: ExampleService) {}
@Get()
getHello(): string {
return this.exampleService.getHello();
}
}
Konstruktor ExampleController automatski prima instancu ExampleService putem DI-a.
Korak 3: Registrovanje u modulu
import { Module } from '@nestjs/common';
import { ExampleService } from './example.service';
import { ExampleController } from './example.controller';
@Module({
controllers: [ExampleController],
providers: [ExampleService],
})
export class ExampleModule {}
Interfejs u DI za veću fleksibilnost
Korišćenje interfejsa u Nest.js omogućava aplikacijama veću fleksibilnost i prilagodljivost. Umesto direktnog korišćenja specifične implementacije, možete definisati interfejs koji opisuje ponašanje (ugovor) koje se očekuje od provajdera. Ovo omogućava lako menjanje implementacije bez potrebe za izmenama u kontrolerima ili drugim zavisnim komponentama.
export interface ExampleInterface {
getHello(): string;
}
Registrovanje interfejsa u modulu
Interfejs i njegova implementacija se registruju kao provajderi unutar modula. Koristi se ključ provide kako bi se povezala implementacija sa interfejsom.
import { Module } from '@nestjs/common';
import { ExampleImplementation } from './example.implementation';
@Module({
providers: [
{
provide: 'ExampleInterface',
useClass: ExampleImplementation,
},
],
})
export class ExampleModule {}
Ovaj pristup omogućava menjanje implementacije jednostavnom zamenom klase u useClass, bez potrebe za promenama u kontrolerima ili servisu.
Implementacija interfejsa
U ovom primeru, ExampleInterface definiše metodu getHello, koja mora biti implementirana od strane bilo koje klase koja se registruje kao provajder.
import { Injectable } from '@nestjs/common';
@Injectable()
export class ExampleImplementation implements ExampleInterface {
getHello(): string {
return 'Hello from implementation!';
}
}
Klasa ExampleImplementation implementira ExampleInterface. Ova implementacija će se koristiti u kontrolerima kada se interfejs
registruje u modulu.
Korišćenje interfejsa u kontroleru
U kontroleru, interfejs se može injektovati koristeći dekorator @Inject(). Ključ ‘ExampleInterface’ se koristi za povezivanje sa odgovarajućim provajderom.
import { Controller, Get, Inject } from '@nestjs/common';
import { ExampleInterface } from './example.interface';
@Controller('example')
export class ExampleController {
constructor(
@Inject('ExampleInterface') private readonly exampleService: ExampleInterface,
) {}
@Get()
getHello(): string {
return this.exampleService.getHello();
}
}
Ovo omogućava da kontroler koristi funkcionalnost definisanu interfejsom, dok implementaciju kontroliše IoC kontejner.
Prednosti ovog pristupa
- Jednostavna zamena implementacije: Možete promeniti klasu koja implementira interfejs bez izmene kontrolera.
- Povećana testabilnost: Umesto stvarne implementacije, tokom testiranja možete registrovati mock ili stub verziju interfejsa.
- Modularnost: Omogućava odvajanje koda u nezavisne module sa minimalnom međuzavisnošću.
- Prilagodljivost: Lako se integrišu različite implementacije, kao što su varijante funkcionalnosti za različite konfiguracije ili okruženja.

