Jednym z kluczowych elementów Angulara jest mechanizm Dependency Injection (DI), który umożliwia łatwe zarządzanie zależnościami w aplikacji. Od początków istnienia frameworka, wstrzykiwanie zależności (injection) było integralną częścią filozofii Angulara. Jednak z czasem, rozwój i zmiany w ekosystemie wymusiły dostosowanie narzędzi DI do nowych potrzeb programistów. W tym kontekście pojawiła się metoda inject()
.
O działaniu mechanizmu DI w Angular poczytasz w naszym obszernym artykule na ten temat.
Kontekst historyczny
Przez wiele lat, wstrzykiwanie zależności w Angularze było realizowane za pomocą konstruktorów klas. Komponenty, serwisy czy dyrektywy mogły definiować swoje zależności w konstruktorze, a Angular automatycznie dostarczał odpowiednie instancje w czasie tworzenia obiektów. To podejście działało świetnie, jednak pojawiły się pewne ograniczenia i wyzwania:
- Złożoność konfiguracji – W miarę rozwoju dużych aplikacji, liczba zależności mogła szybko rosnąć, co powodowało skomplikowaną konfigurację w konstruktorach.
- Testowanie – Wstrzykiwanie zależności przez konstruktor bywało problematyczne podczas testowania, szczególnie przy mockowaniu zależności.
- Funkcje a nie klasy – W Angularze pojawiały się scenariusze, w których programiści chcieli korzystać z mechanizmu DI w kontekście funkcji, a nie tylko klas. Konstruktorowe podejście nie dawało możliwości korzystania z DI poza kontekstem klas.
Aby rozwiązać te problemy, w Angularze 14 wprowadzono nową metodę o nazwie inject()
. Metoda ta umożliwia dostęp do zależności w dowolnym miejscu kodu, nie tylko w konstruktorze klasy, co otwiera nowe możliwości dla programistów.
Uproszczenie dziedziczenia
Jednym z wyzwań, z jakimi programiści spotykali się w Angularze przed wprowadzeniem metody inject()
, było dziedziczenie klas serwisów, komponentów lub innych jednostek posiadających zależności. W tradycyjnym podejściu do Dependency Injection, gdy klasa dziedziczyła po innej klasie, trzeba było przekazać wszystkie zależności do konstruktora rodzica za pomocą funkcji super()
. Mogło to być kłopotliwe, szczególnie w dużych aplikacjach, gdzie liczba zależności szybko rosła.
Tradycyjne dziedziczenie z konstruktorami
W typowej implementacji Angulara przed wprowadzeniem inject()
, jeśli klasa B dziedziczyła po klasie A, a obie klasy miały swoje własne zależności, konieczne było jawne przekazywanie tych zależności do konstruktora klasy bazowej.
Oto przykład:
class A {
constructor(private serviceA: ServiceA, private serviceB: ServiceB) {}
}
class B extends A {
constructor(serviceA: ServiceA, serviceB: ServiceB, private serviceC: ServiceC) {
super(serviceA, serviceB); // Musimy ręcznie przekazać zależności
}
}
Jak widać, w tym podejściu, aby klasa B mogła poprawnie zainicjalizować klasę A, programista musiał ręcznie przekazywać wszystkie zależności do metody super()
. W miarę jak liczba zależności rosła, kod stawał się coraz bardziej skomplikowany i podatny na błędy.
Wykorzystanie inject()
class A {
private serviceA = inject(ServiceA);
private serviceB = inject(ServiceB);
}
class B extends A {
private serviceC = inject(ServiceC);
}
W tym podejściu:
Klasa B dziedziczy po klasie A, ale nie musi już przekazywać zależności do konstruktora rodzica. Zamiast tego, również używa metody inject()
do wstrzyknięcia własnych zależności (ServiceC
).
Klasa A używa metody inject()
do wstrzyknięcia swoich zależności (ServiceA
i ServiceB
).
Tworzenie reużywalnych metod utilities
Dzięki inject()
możemy zdefiniować reużywalną funkcję wykonującą operacje na routerze, store czy signals, która może być wywoływana w dowolnym miejscu aplikacji bez konieczności pisania dodatkowego kodu.
Przykład (funkcja pobierająca parametr page z ActivatedRoute):
const getPageParam = () => {
return inject(ActivatedRoute).queryParams.pipe(
filter((params) => params['page']),
map((params) => params['page'])
);
}
@Component()
export class AppComponent {
page$ = getPageParam();
}
Wstrzykiwanie zależności do guardów
W Angularze wersji 16 (i późniejszych) pojawiły się istotne zmiany w sposobie tworzenia guardów, a interfejsy takie jak CanActivate
, CanActivateChild
, CanDeactivate
, CanLoad
i inne związane z ochroną tras zostały oznaczone jako przestarzałe (deprecated). Zostały one zastąpione przez nowe funkcje oparte na strumieniach (observables
), sygnałach (signals
) i funkcjach.
Angular przechodzi w kierunku bardziej reaktywnego podejścia do zarządzania ochroną tras. Wersja 16 wprowadziła nowy sposób definiowania guardów, który eliminuje potrzebę implementacji interfejsów. Wraz z tą zmianą znika możliwość wstrzykiwania do nich zależności przez konstruktor.
Przykładowa implementacja:
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard = () => {
const authService = inject(AuthService); // Wstrzykujemy AuthService za pomocą inject()
const router = inject(Router); // Wstrzykujemy Router
return authService.isAuthenticated() ? true : router.parseUrl('/login');
};
Podsumowanie
Metoda inject()
w Angularze wprowadza nowy sposób zarządzania zależnościami, który znacząco upraszcza dziedziczenie. Dzięki temu programiści mogą skupić się na logice aplikacji, zamiast martwić się o ręczne przekazywanie zależności do konstruktorów klas bazowych. Dostajemy też nowe możliwości tworzenia reużywalnych funkcji utility, które mogą operować na różnych zależnościach takich jak nawigacja, zarządzanie stanem, oraz sygnały. Zmienił się także sposób tworzenia guardów. Choć tradycyjne wstrzykiwanie przez konstruktor jest wciąż dostępne, to mimo wszystko gorąco zachęcam do korzystania wyłącznie z inject(). Mieszanie obu rozwiązań w ramach jednej aplikacji może wprowadzić niemały chaos, a korzyści zastosowania inject() są znaczne i dlatego warto dokonać migracji w całej aplikacji.
One reply on “Angular – inject() czy wstrzykiwanie przez konstruktor?”
[…] z najważniejszych i najbardziej zauważalnych nowości w Angular 17 jest wprowadzenie nowego systemu control flow w templatkach. Jest to fundamentalna zmiana, […]