Dependency Injection (Zavisna injekcija) je dizajn patern i osnovni deo Angular arhitekture koji omogućava komponentama, servisima i drugim klasama da prijave svoje zavisnosti i da im budu automatski prosleđene od strane frejmvorka, umesto da ih ručno kreirate.
Zašto je to važno:
- Odvaja klase od njihovih zavisnosti
- Olakašava testiranje i ponovnu upotrebu
- Podstiče modularnu arhitekturu laku za održavanje
Mehanizam Zavisne Injekcije
Angular omogućava klasama sa Angular dekoraterima, kao što su Components, Directives, Pipes i Injectables, da konfigurišu zavisnosti putem Injector Stabla. Injector Stablo je hijerarhijska struktura injektora koju Angular koristi da utvrdi kako i gde da rezolvuje zatražene zavisnosti, počevši od nivoa komponente i idući nagore ka globalnim nivoima.
Uzmimo u obzir ovu ručno-povezanu klasu:
class Api {
endpointUrl: string;
http = new HttpClient(new OurHttpHandler());
constructor(endpointUrl: string) {
this.endpointUrl = endpointUrl;
}
}
Ovo stvara duboko ugnježdenu strukturu objekata:
{
endpointUrl: 'http://localhost:3333/api',
http: {
handler: {
handlerProps: {
prop: { ... }
}
},
post(...) { ... }
}
}
U čemu je problem:
-
HttpClientzahtevaHttpHandler, koji je apstraktna klasa, što znači da morate ručno da je implementirate i instancirate. - Čvrsto vezujete vašu klasu za implementacione detalje niskog nivoa.
- Kako kompleksnost raste, ovo postaje teško za održavanje, testiranje i ponovnu upotrebu.
Angularov DI mehanizam uklanja potrebu za ručnom instancijacijom tako što:
- Upravlja kreiranjem objekata "iza kulisa"
- Deli instance kroz vašu aplikaciju
- Čini klase lakšim za mock-ovanje, zamenu i testiranje
Umesto da kreiramo instancu klase kao iznad, možemo injektovati zavisnost
koristeći inject funkciju, nešto ovako:
@Injectable({ providedIn: 'root' })
export class Api {
private endpointUrl = inject(API_ENDPOINT_URL);
private http = inject(HttpClient);
}
Umesto kreiranja i ručnog prosleđivanja duboko ugnježdenih zavisnosti, Angular-ov Dependency Injection mehanizam omogućava klasama da deklarišu šta im je potrebno, a stablo injektora to obezbeđuje, poboljšavajući jasnoću koda, skalabilnost i lakoću testiranja.
Takođe, možemo injektovati plain objekte ili promenljive koristeći Injection Token kao što smo uradili za
endpointUrl property. Više o tome kasnije.
Stablo Injektora (Injector Tree)
Prilikom pokretanja Angular aplikacije korišćenjem
bootstrapApplication (standalone APIs) ili
bootstrapModule (NgModules), Angular inicijalizuje hijerarhijski sistem Zavisne Injekcije (DI), često nazvan Stablo Injektora.
Null Injektor
Na samom dnu stabla injektora nalazi se Null Injektor. On služi kao poslednje utočište. Ako tražena zavisnost ne može biti pronađena u bilo kom drugom injektoru, Null Injektor baca runtime grešku:
NullInjectorError: No provider for <ServiceName>!
Njegova jedina odgovornost je da brzo izbaci grešku (fail fast) ukoliko je rezolucija zavisnosti dotakla dno bez rezultata.
Platform Injektor
Odmah iznad Null Injektora nalazi se Platform Injektor. Kreira se kada se Angular prvi put učitava
(kroz platformBrowser ili platformBrowserDynamic).
On sadrži globalne singltone kao što je Angular Kompajler, koji bi trebalo da se deli širom:
- Cele aplikacije.
- Više Angular aplikacija koje se pokreću na istoj stranici.
Ovaj sloj se retko menja, ali je bitan za podršku naprednih slučajeva i logike učitavanja.
Root Injektor
Root Injektor je centar Angular-ove DI hijerarhije i odgovoran je za pružanje (provide-ovanje) singlton servisa širom aplikacije.
-
Servisi dekorisani sa
@Injectable({ providedIn: 'root' })su registrovani sa ovim injektorom. - Ovi servisi su instancirani jednom i dele se na celu aplikaciju, što ih čini efikasnim i lako dostupnim.
Dodatno, Angular dozvoljava da servisi i tokeni za injekciju budu prosleđeni preko Aplikacionog Konfiguracionog objekta prilikom korišćenja standalone API-ja:
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
]
});
Sada možemo koristiti HttpClient injektable klasu širom aplikacije.
Element Injektor
Element Injektor je injektor na nivou instance kreiran za svaku komponentu ili direktivu u komponentskom stablu. On je odgovoran za rešavanje zavisnosti koje se pružaju lokalno, na nivou komponente ili direktive.
@Component({
selector: 'mk-widget',
providers: [WidgetService]
})
export class WidgetComponent {
private readonly widgetService = inject(WidgetService);
}
Dodavanjem provajdera (WidgetService) u providers niz komponentinog
dekoratera, svaki put kada se kreira komponenta, kreira se i nova instanca
WidgetService-a a njen životni vek menjaće se sa životnim vekom
komponente. Takođe, ova instanca će biti dostupna u scopes-u (prostoru) dece ove
komponente/direktive.
Servisi & Injection Tokeni
Angularov sistem Zavisne Injekcije izgrađen je oko provajdera, koji asociraju tokene sa
zavisnostima (obično servisima). Ovi tokeni mogu biti klase (tipovi) ili
instance InjectionToken-a.
Kad god tražimo zavisnost, mi se spuštamo niz Element Injektor do Null Injektora gde možemo dobiti grešku ukoliko nikakav provajder nije pronađen za tu zavisnost.
Servisi
Servis u Angularu obično predstavlja klasu dekorisanu sa @Injectable() namenjenu za enkapusuliranje višekratke poslovne logike, stanja ili pomoćnih funkcionalnosti.
Servisi mogu biti singltoni (ako su provide-ovani u root-u) ili ograničeni (ako su provide-ovani na nivou komponente, direktive, modula ili rute).
Injection Tokeni
U Angularu, Injection Tokeni predstavljaju način da obavestite Dependency Injection sistem šta tačno treba da se injektuje, posebno kada ne injektujete klasu. Oni su korisni kada:
- Injektujete primitive (string, number, boolean)
- Injektujete konfiguracioni objekat
- Imate više implementacija istog interfejsa/tipa (aliasing)
- Primenjujete jako tipiziranje na nešto što inače nema definisanu klasu
Uzmimo u obzir već spomenuti primer za API Servis. Umesto da koristite konstruktor ili druge metode za zadavanje endpoint-a, možemo to injektovati kao konstantnu varijablu.
import { InjectionToken } from '@angular/core';
export const API_ENDPOINT_URL = new InjectionToken<string>('API_ENDPOINT_URL', {
providedIn: 'root',
factory: () => {
return 'https://dummyjson.com';
},
});
Kao što vidite, naš Injection Token takođe ima polje providedIn: 'root' a sa factory funkcijom koja mu dodeljuje vrednost, spreman je za rad.
@Injectable({
providedIn: 'root',
})
export class Api {
private readonly endpointUrl = inject(API_ENDPOINT_URL);
private readonly http = inject(HttpClient);
post<T, G = any | null>(path: string, body: G) {
return this.http.post<T>(`${this.ENDPOINT}/${path}`, body);
}
}
Opozivanje i Zamena Provajdera
Podrazumevano, servisi proved-ovani na root-u su singltoni. Ali vi možete da ih premostite (override) na nivou Rutera/Modula/Komponente/Direktive:
@Component({
selector: 'mk-widget',
providers: [WidgetService]
})
export class WidgetComponent {...}
Ovo dozvoljava izuzetno finu kontrolu, korisno je za:
- Testiranje
- Logiku vezanu za specifične feature-e
- Development/debug okruženja
Primer iz stvarnog sveta
Zamislimo da implementiramo Payments (Plaćanja) servis koji će se ponašati drugačije na osnovu toga da li korisnik plaća preko PayPal-a ili ApplePay servisa.
ng generate service core/services/payments
import { inject, Injectable } from '@angular/core';
import { Api } from './api';
@Injectable({
providedIn: 'root',
})
export abstract class Payments {
protected readonly api = inject(Api);
abstract pay: (amount: number) => number;
}
@Injectable({
providedIn: 'root',
})
export class ApplePayPayments extends Payments {
pay = (amount: number) => amount + 0.003;
}
@Injectable({
providedIn: 'root',
})
export class PayPalPayments extends Payments {
pay = (amount: number) => amount + 0.002;
}
useExisting:
NullInjectorError: No provider for ApplePayPayments!
useClass:
@Component({
selector: 'app-apple-pay-widget',
providers: [{ provide: Payments, useClass: ApplePayPayments }],
})
export class ApplePayWidget extends PaymentWidget {
useValue:
@Component({
providers: [{ provide: Payments, useValue: { pay(amount: number) { return amount; } } }],
})
export class ApplePayWidget extends PaymentWidget {
useFactory:
providers: [{
provide: Payments,
useFactory: () => {
const auth = inject(Auth);
const role = auth.role();
return role === Role.DEV ? FakeApplePayPayments : ApplePayPayments;
}
}]
multi: true
Dozvoljava raznim provajderima za ista imena tokena da donesu sve potrebne varijacije zajedno u nizu. Često viđeno pri formiranju Http intercepting faza uz pomoć HttpInterceptors modula.
Zaključak
Dependency Injection predstavlja više od koncepta iz prve linije - on u Angular sistemu diktira glavnu kičmu celokupne mase logičnih skalabilnih sistema koji su idealni za test i održavanje!
Sledeći put kada injektujete servis, setite se: vi niste samo povukli klasu. Vi se penjete uz stablo zavisnosti.