Nazad na blog
9 min čitanja

Razumevanje Dependency Injection u Angularu

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:

  • HttpClient zahteva HttpHandler, 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.

  • Identifikujte uska grla u performansama
  • Uočite probleme u arhitekturi
  • Dobijte konkretne preporuke
Pokreni Besplatnu Analizu