Wróć do strony głównej
Angular

Angular & Dependency Inversion Principle

Wstęp

To już ostatni artykuł w serii dotyczącej skrótu SOLID. Jest to zbiór zasad, dzięki którym możemy pisać kod, który łatwiej będzie nam skalować, oraz zmieniać zachowanie naszej aplikacji, bez modyfikowania znacznej ilości kodu.

 

Na zbiór zasad składają się:

 

Dzisiaj zajmiemy się Dependency Inversion Principle 🙂

Dependency Inversion Principle

https://www.abhishekshukla.com/net-2/dependency-inversion-principle-dip/

Tak jak widzimy na obrazku, korzystając z urządzeń elektrycznych, raczej nie wlutowujemy ich bezpośrednio do instalacji elektrycznej. Zamiast tego po prostu podłączamy urządzenie do gniazdka 🙂 

 

Zatem regułę tę można sobie wyobrazić jako tworzenie “gniazdek” w naszym kodzie, do których będziemy mogli wymiennie podłączać inne urządzenia (serwisy, funkcje itd.).

 

Formalna definicja

Brzmi ona tak:

  • wysokopoziomowe moduły nie powinny zależeć od niskopoziomowych modułów
  • oba powinny zależeć od abstrakcji
  • abstrakcje nie powinny zawierać szczegółów (bo te powinny być już w konkretnej implementacji)

 

Co nam daje zachowanie tej reguły?

  • łatwo reużywalne, wysokopoziomowe moduły (tzw. building blocks aplikacji)
  • zmiany w niskopoziomowych modułach nie powinny wpływać na te wysokopoziomowe. Czyli możliwość zmiany zachowania bez modyfikowania dużej części aplikacji

 

Tak więc podsumowując:

  • wysokopoziomowy moduł musi zależeć od abstrakcji (definiować ją – tworzyć interfejs)
  • niskopoziomowy moduł musi też zależeć od tej samej abstrakcji (implementować ją – dostarczać implementację interfejsu)

Przykłady

Najprostszy przykład to Pipe w Angularze. Gdyby nie ten interfejs, nie dałoby się dodawać własnych Pipe’ów do naszych aplikacji (bo wtedy trzeba by było dodawać if’a obsługującego naszego konkretnego pipe’a w kodzie Angulara).

Wysokopoziomowy moduł: Angular – zależy od abstrakcji (definiuje interfejs)

Niskopoziomowy moduł: nasza aplikacja – implementuje abstrakcję (implementuje interfejs)

Kolejny przykład.

Załóżmy że mamy aplikację do zamówień w sklepie internetowym. W związku z tym musimy obliczać podatek w zamówieniu.

Jako wysokopoziomowy moduł mamy tu komponent ze wstrzykniętym serwisem.

Jako niskopoziomowy moduł mamy serwis.

 

Dopóki nasza aplikacja działa w obrębie jednego państwa, wszystko jest proste. Jednak co w przypadku, gdy chcielibyśmy wkroczyć na inne rynki? Jak obliczać podatek dla różnych państw?

 

Naiwne rozwiązanie:

Serwis, który na podstawie przesłanego kodu kraju zwróci odpowiednią wartość:

Problem: co jeśli chcemy obsłużyć w naszej aplikacji kolejny kraj? Musimy dopisać “ifa”.

 

A co jeśli podejdziemy do problemu w sposób bardziej abstrakcyjny? Poszukujemy przecież serwisu, który obliczy podatek dla danego kraju. Wydzielmy więc interfejs, który będziemy implementować w zależności od potrzeby:

PS Jest to wzorzec projektowy strategii 🙂

 

Po wydzieleniu interfejsu, możemy przejść do implementacji.

Teraz w komponencie będziemy korzystać z abstrakcji (interfejsu), a nie konkretnej implementacji.

 

Spójrzmy, jak możemy teraz dostarczyć odpowiednią implementację na poziomie modułu.

 

Ciekawostka:
Co jeśli na poziomie modułu nie wiemy, jakiej implementacji chcemy użyć? Tzn. chcemy dostarczyć implementację “na bieżąco”, w trybie “Live” 🙂

 

Załóżmy że dostajemy kod kraju jako parametr w URL route’a.

Zdefiniujmy więc fabrykę, która będzie nam dostarczać odpowiednie implementacje na podstawie przesłanego kodu kraju:

Tę fabrykę wstrzykujemy sobie do komponentu:

 

Przejdźmy do kolejnego przykładu:

Załóżmy, że mamy serwis, który robi CRUD operacje na encji (w postaci requestów HTTP):

Co jest nie tak? Na pierwszy rzut oka nic.

Problem pojawia się gdybyśmy chcieli eksperymentalnie wprowadzić obsługę GraphQL na jednym ze środowisk. Wtedy musielibyśmy dodać w każdej metodzie ifa sprawdzającego środowisko:

Problem – modyfikujemy istniejący kod, wprowadzając sprawdzanie ifem środowiska. Gdybyśmy chcieli na jeszcze innym środowisku użyć np. WebSocketów, dodalibyśmy kolejnego ifa, i kolejną zależność do serwisu.

 

Jak to rozwiązać?

Wydzielmy interfejs:

Zmieńmy użycia z konkretnej implementacji na abstrakcję (interfejs).

Dostarczmy konkretną implementację w zależności od środowiska na poziomie modułu:

W ten sposób zachowujemy regułę Dependency Inversion Principle. Przy okazji polecam również artykuł, w którym pokazane jest zachowanie tej reguły przy połączeniu Angulara z NestJS – https://angular.love/2020/12/02/jak-postepowac-zgodnie-z-zasada-odwrocenia-zaleznosci-dip-w-nestjs-i-angular/

 

O autorze

Wojciech Janaszek

Jestem Angular i NestJS developerem. Swoją przygodę zaczynałem od pierwszych wersji „nowego” Angulara (2 wzwyż). Od niedawna korzystam również z NestJS (którego nauka znając dobrze Angulara przychodzi łatwo – wszystko jest bardzo podobne). W swojej pracy dużą uwagę przykładam do tzw. clean code i clean architecture. Lubię mieć “porządek” w kodzie 🙂 W wolnym czasie interesuję się sportowymi samochodami, ogólnie pojętym motorsportem. Gram również amatorsko w siatkówkę.

Zapisz się do naszego newslettera. Bądź na bieżąco z najnowszymi trendami, poradami, meetupami i stań się częścią społeczności Angulara w Polsce. Rynek pracy docenia członków społeczności.

Jeden komentarz

  1. Mariusz

    Cześć! Dzięki za ten artykuł, bardzo pomocny. Mam jedno pytanie: dwukrotnie w tekście piszesz, że tworzysz interfejs ale w przykładnie podajesz klasę abstrakcyjną. Czy w tym przypadku faktycznie nie lepiej byłoby użyć interfejsu jeśli mamy same metody abstrakcyjne?
    Pozdrawiam!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *