Š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:
1 2 3 4 5 6 7 8 9 10 11 |
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:
1 2 3 4 5 6 7 8 9 10 |
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
1 2 3 4 5 6 |
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.
12345678910@Module({providers: [{provide: SmtpService,useClass: AlternativeEmailService, // Zamena implementacije},EmailService,],})export class AppModule {} -
Testiranje: Lako se mockuje zavisnost u testovima:
12const 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
1 2 3 4 5 6 7 8 |
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
1 2 3 4 5 6 7 8 9 10 11 12 |
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
1 2 3 4 5 6 7 8 9 |
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.
1 2 3 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 |
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.
1 2 3 4 5 6 7 8 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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.