9.11.20226 min

Szymon Biduła Blockchain DeveloperEspeo Software

Przewodnik po Dependency Injection w NEST.JS

Poznaj szczegóły wzorca Dependecy Injection, czyli wstrzykiwania zależności, oraz metody jego implementacji.

Przewodnik po Dependency Injection w NEST.JS

Wstrzykiwanie zależności (ang. Dependency Injection, DI) to jeden z najpopularniejszych wzorców w projektowaniu oprogramowania. Szczególnie wśród programistów, którzy przestrzegają zasad czystego kodu. Co zaskakujące, różnice i relacje między zasadą odwrócenia zależności (dependency inversion principle), odwróceniem kontroli (reversion of control) i wstrzykiwaniem zależności (dependency injection) zazwyczaj nie są jasno określone. 

Dlatego, wpis ten ma na celu wyjaśnienie granic między tymi terminami. W formie przystępnego przewodnika, przedstawię w jaki sposób można zaimplementować i wykorzystać wstrzykiwanie zależności (DI). 

Podsumowując, po przeczytaniu tego materiału dowiesz się:

  • Jaka jest różnica między zasadą odwrócenia zależności, odwróceniem kontroli i wstrzykiwaniem zależności?
  • Dlaczego wstrzykiwanie zależności jest przydatne?
  • Czym są metaprogramowanie, dekoratory i Reflection API?
  • Jak zaimplementowano wstrzykiwanie zależności w NestJS?


Wstęp 

Projektowanie oprogramowania stale się rozwija. Pomimo ogromnych implikacji praktycznych, wciąż często nie potrafimy odróżnić świetnego kodu aplikacji od tego niskiej jakości. Dlatego myślę, że warto kontynuować dyskusję na temat tego, jak scharakteryzować świetny kod.

Jedna z najpopularniejszych opinii głosi, że Twój kod musi być czysty, aby był dobry.

Ten sposób myślenia jest szczególnie dominująca wśród programistów korzystających z paradygmatu programowania obiektowego. Dlatego proponują dekompozycję części rzeczywistości objętej aplikacją na obiekty definiowane przez klasy. W związku z tym, pogrupuje je na trzy warstwy:

  • Interfejs użytkownika (UI);
  • Logika biznesowa;
  • Warstwa implementacji (bazy danych, sieci itd.).

Zwolennicy czystego kodu uważają, że te warstwy nie są równe i istnieje między nimi hierarchia. Mianowicie implementacja i warstwy interfejsu użytkownika powinny zależeć od logiki biznesowej. Wstrzykiwanie zależności (DI) to wzorzec projektowy, który pomaga osiągnąć ten cel.

Zanim przejdziemy dalej, musimy poczynić pewne wyjaśnienia. Choć tradycyjnie wyróżniamy trzy warstwy aplikacji, ich liczba mogłaby być większa. Warstwy „wyższego poziomu” są bardziej abstrakcyjne i odległe od operacji wejścia/wyjścia.


Poznajmy zasadę odwrócenia zależności (Dependency Inversion Principle, DIP)

Zwolennicy czystej architektury wskazują, że dobrą architekturę oprogramowania można opisać jako niezależność warstw wyższego poziomu od modułów niższego poziomu. Nazywa się to zasadą odwrócenia zależności (DIP). Słowo „inwersja” wywodzi się z odwrócenia tradycyjnego przepływu aplikacji, w którym interfejs użytkownika zależał od logiki biznesowej. Z kolei logika biznesowa zależała od warstwy implementacyjnej (jeśli zostały zdefiniowane trzy warstwy). W architekturze, w której twórcy oprogramowania stosują się do DIP, warstwy interfejsu użytkownika i implementacji zależą od logiki biznesowej.

Kilka technik programowania zostało wynalezionych, aby osiągnąć DIP. W związku z tym są one określane razem pod wspólnym terminem: odwrócenie kontroli (Inversion of control, IoC). Wzorzec wstrzykiwania zależności pomaga odwrócić kontrolę nad programem podczas tworzenia obiektów. Tak więc IoC nie ogranicza się do wzorca DI. Poniżej znajdziesz kilka innych przypadków użycia IoC:

  • Zmiana algorytmów dla określonej procedury w locie (wzorzec strategii),
  • Aktualizacja obiektu w czasie wykonywania (wzór dekoratora),
  • Informowanie subskrybowanych obiektów o zmianie stanu (wzorzec obserwatora).


Cele i zalety wstrzykiwania zależności (DI)

Jeśli używasz technik, które odwracają przepływ aplikacji, wiąże się to z ciężarem pisania/używania dodatkowych komponentów. Niestety zwiększa to koszt utrzymania. Rzeczywiście, rozszerzając w ten sposób bazę kodu, zwiększamy również złożoność systemu. W efekcie bariera wejścia dla nowych deweloperów jest wyższa. Dlatego warto omówić potencjalne korzyści płynące z IoC i to kiedy warto z niego korzystać.

Aby opisać wstrzykiwanie zależności (DI), oddzielamy inicjalizację obiektów używanych przez klasę od samej klasy. Innymi słowy, oddzielamy konfigurację klasy od jej zainteresowania. Zależności klasowe są zwykle nazywane usługami. Tymczasem opisana jest klasa – klient.

W tym przypadku założyłem, że wzorzec DI opiera się na wstawieniu zainicjowanych usług jako argumentów do konstruktora klienta. Istnieją jednak inne formy tego wzorca. Przykładem może być dyspozytor wstrzykiwania interfejsu. Na potrzeby tego wpisu, opisuję tylko najpopularniejszy wstrzyknięcie konstruktora.

Ogólnie rzecz biorąc, DI pozwala aplikacjom stać się:

  • Bardziej odpornymi na zmiany. W przypadku zmiany szczegółów usługi, kod klienta pozostaje taki sam. Tak więc zależności są wstrzykiwane przez kod i poza klientem.
  • Bardziej testowalnymi. Skutkuje to także spadkiem kosztów pisania testów aplikacji.

Kiedy te korzyści są warte wysiłku programistów? Odpowiedź jest prosta! Gdy system ma być długowieczny i obejmuje dużą domenę, co skutkuje złożonym grafem zależności.


Ramy techniczne wdrożenia wstrzykiwania zależności (DI)

Po pierwsze, powinniśmy rozważyć i zrozumieć zalety i wady DI. Jak jest w Twoim przypadku? Czy Twój system jest wystarczająco duży, aby skorzystać z tego wzorca? Jeśli tak, to dobry moment na zastanowienie się nad wdrożeniem. 

Ten temat zabierze nas w podróż metaprogramowania. Zobaczmy, jak stworzyć program skanujący kod, a następnie działający na podstawie zebranych danych. 

Czego potrzebujemy stworzyć framework wykonujący DI? Zwróćmy uwagę na kilka punktów konstrukcyjnych:

  1. Po pierwsze potrzebujemy miejsca do przechowywania informacji o zależnościach. To miejsce jest zwykle nazywane kontenerem.
  2. Wtedy nasz system potrzebuje wtryskiwacza. Musimy więc zainicjować usługę i wstrzyknąć zależność do klienta.
  3. Następnie potrzebujemy skanera. Niezbędna jest możliwość przejrzenia wszystkich modułów systemu i umieszczenia ich zależności w kontenerze.
  4. Na koniec będziemy potrzebować sposobu na adnotowanie klas za pomocą metadanych. Pozwala to systemowi zidentyfikować, które klasy powinny zostać wstrzyknięte, a które będą adresatem wstrzyknięcia.


TypeScript, dekoratory i interfejs API refleksji

W  odniesieniu do punktów ogólnych wymienionych powyżej, warto skupić się na warunkach struktur danych, które mogą nam pomóc w implementacji specyfikacji.

W sferze TypeScript możemy z łatwością użyć:

  • Refleksji interfejsu programowania aplikacji (API). Ten interfejs API to zbiór narzędzi do obsługi metadanych (np. dodawania, pobierania, usuwania itd.).
  • Dekoratorów. Te struktury danych to funkcje uruchamiane podczas analizy kodu. Do uruchomienia odbicia można użyć interfejsu API. Następnie klasy zostaną opatrzone adnotacjami z metadanymi.


Implementacja wstrzykiwania zależności to NestJS

To była długa podróż! Cieszę się że wytrwałeś do tego momentu. Zebraliśmy wszystkie elementy, aby zrozumieć, jak zaimplementować wstrzykiwanie zależności w jednym z najpopularniejszych frameworków NodeJS – NestJS.

Spójrzmy na kod źródłowy frameworka. Proszę sprawdź ten link

Tutaj interesują nas dwie definicje dekoratorów.

Oba te pliki eksportują funkcje dodające metadane do obiektu. Metoda uruchamia funkcję defined metadata z refleksji interfejsu API.

W pierwszym przypadku (Injectable.decorator.ts) oznaczamy klasy jako dostawców. W związku z tym mogą być wstrzykiwane przez system zależności NestJS. Ten ostatni przypadek (Injectable.decorator.ts) jest inny. Informujemy system DI, że musi podać konkretny parametr konstruktora z klasy.

NestJS grupuje dostawców i inne klasy w grupy o wyższej architekturze zwane modułami. Używamy dekoratorów znajdujących się nest/packages/common/decorators/modules, jako module.decorator.ts

 Podobnie jak w przypadku dekoratorów zdefiniowanych powyżej, główną rolą tego kodu jest eksportować funkcję, która uruchamia refleksję interfejsu API. Ta klasa jest modułem, wobec którego musimy odpowiedzieć na dwa zasadnicze pytania.

  1. Które klasy są kontrolerami?
  2. Kim są dostawcy? 

Wtedy system DI może ocenić, jak utworzyć wystąpienie zależności.

NestJS dodaje metadane, które pozwala podzielić kod w logiczne jednostki:

  • Jednostki dostawców iniekcji.
  • Parametry konstruktorów potrzebne do wstrzykiwania.
  • Moduły budujące graf zależności projektu.

Implementacja krok po kroku w NESTJS

W jaki sposób NestJS wykorzystuje informacje? Każdy projekt NestJS zaczyna się mniej więcej podobnym stwierdzeniem:

const app = await NestFactory.create()


Uruchamia ono funkcję create z NestFactoryStatic, która inicjuje między innymi funkcję Initialize.

Jakie jest zadanie funkcji initalize?

  • Funkcja tworzy dependency scanner, który skanuje moduły w poszukiwaniu zależności.
  • Podczas scanForModules dodajemy moduły do ​​kontenera, który jest Mapą pomiędzy Stringiem a Modułem.
  • Następnie uruchamiany jest scanModulesForDependencies, który w istocie uruchamia 4 funkcje: reflectImports, reflectProviders, reflectControllers i reflectExports.
  • Te funkcje mają podobny cel: uzyskać metadane opatrzone adnotacjami przez dekoratory i wykonać określone akcje.
  • Następnie,  instanceLoader inicjuje zależności, uruchamiając funkcję createInstancesOfDependencies, która tworzy i ładuje odpowiedni obiekt.

Opisany obraz jest mniej skomplikowany niż cały kod systemu. Musi obsługiwać skrajne przypadki zależności kołowych, i innych, a przy tym dalej przekazywać jego sedno. 


Podsumowanie 

Podsumujmy naszą podróż! Najpierw dowiedzieliśmy się, że klasy są pogrupowane w warstwy. Nie są też równe. Aby zachować wśród nich właściwą hierarchię, musimy odwrócić kontrolę. Innymi słowy, musimy ujednolicić przepływ aplikacji.

W przypadku tworzenia obiektów możemy odwrócić sterowanie za pomocą systemu wstrzykiwania zależności (DI). Świetnym przykładem jest NestJS. Tutaj system działa z wykorzystaniem dekoratorów i interfejsu refleksji API. Dlatego, umożliwia transformację metaprogramowania w TypeScript.

Krótko mówiąc, cały ten proces jest wart wysiłku w przypadku złożonych i długotrwałych zastosowań.

Źródła 

 
<p>Loading...</p>