Dependency Injection

W trakcie nauki programowania prędzej czy później spotkamy się z wzorcami projektowymi. Gdy już je poznamy okazuje się, że są wszędzie i stosujemy je nawet nie mając świadomości, że to robimy. Dziś o jednym z wzorców projektowych - Dependency Injection.

Botox Injections PAGES.POST.COVER_THUMBNAIL.BY_WHOM Jacob Lund

Z tego artykułu dowiesz się:

  • Jak działa zasada Dependency Inversion Principle

  • Co to jest Inverstion of Control

  • Co to jest Dependency Injection

  • Co to jest Zasada Hollywood

  • Jakie mamy rodzaje wstrzykiwania zależności

  • Jakie są wady i zalety Dependecy Injection

Już od dłuższego czasu miałem w planach rozpocząć serię związaną z wzorcami projektowymi. Wzorce projektowe to po prostu, uznane za optymalne, rozwiązania powszechnych problemów, które napotykamy podczas programowania. Idąc tym tropem wzorzec projektowy nie jest konkretnym fragmentem kodu, który możemy zastosować, aby rozwiązać napotkany problem. Jest to raczej koncepcja, którą możemy dostosować do projektu, nad którym pracujemy.

Czy to moja zależność?

Tworzone przez nas aplikacje bardzo często składają się z wielu różnych elementów. Fragmenty te, na przykład w postaci klas, są odpowiedzialne za odrębne aspekty naszej logiki biznesowej. Wielokrotnie jednak możemy dojść do wniosku, że jedna część naszej aplikacji potrzebuje skorzystać z logiki zaszytej w innej jej części. Gdy takich zależności w naszym kodzie powstaje coraz więcej, rodzi się chaos. Jednakże chaos związany z ilością powiązań nie jest jedynym zagrożeniem dla naszego kodu.

Ostatnia z zasad SOLID, czyli Dependency Inversion Principle, mówi nam o tym, że powiązania pomiędzy naszymi klasami powinny być luźne. Oznacza to, że klasy wyższego poziomu nie powinny być zależne od klas niższego poziomu, a abstrakcje nie powinny zależeć od szczegółów (powinno być odwrotnie). Innymi słowy szczegół implementacyjny jest czymś w naszej aplikacji, co powinno znaleźć się na najwyższej warstwie. Dokładnie tak, jak w prawdziwym świecie - ludzie należą do królestwa zwierząt, typu strunowców, podtypu kręgowców, grupy żuchwowców, gromady ssaków, rzędu naczelnych, rodziny człowiekowatych i ostatecznie gatunku Homo Sapiens. Zwróć uwagę, że im wyżej w tej systematyce, tym bardziej szczegółowe są cechy opisywanych zwierząt. Przykładowo bezsprzeczny brak ogona jest cechą człowiekowatych, ale już nie naczelnych. Do królestwa zwierząt zaliczymy natomiast wielokomórkowe organizmy cudzożywne o komórkach z jądrem komórkowym. Zasadniczo i człowiek, i tasiemiec. Trudno natomiast byłoby stwierdzić, że tasiemiec należy do naszego gatunku. Na pewnym poziomie abstrakcji możemy zatem założyć, że całe życie na ziemi jest takie samo, ale to szczegóły powodują różnorodność.

W programowaniu jest identycznie. Abstrakcja pomaga opisać nam ogólne założenia, ale szczegóły implementacyjne będą gdzieś wyżej i spowodują, że nasze klasy będą służyły innym celom. Przesunięcie zbyt dużej szczegółowości na warstwę abstrakcji może zaburzyć logikę w naszej aplikacji i dodatkowo skomplikować kod, którego utrzymanie i rozwijanie stanie się dużo bardziej wymagające.

Gdybyśmy mieli napisać program do obsługi semi-autonomicznych aut, zapewne posiadalibyśmy sporo funkcjonalności z powiadamianiem kierowcy o zagrożeniach na drodze. Zagrożenia natomiast dostarczane byłyby z różnych części aplikacji odpowiedzialnych za: utrzymanie pasa ruchu czy analizę znaków drogowych.

W powyższym przykładzie serwis odpowiedzialny za powiadomienia jest „zaszyty” na stałe w klasie Car. Taka konstrukcja oznacza, że klasa Car musi wiedzieć wszystko o zależnościach serwisu powiadomień. Łatwo zauważyć, że poziom wygenerowanych w ten sposób powiązań zależności może być bardzo wysoki.

Dodatkowo zmiany w klasie NotificationService wymuszą zmiany w klasie Car. Wygląda to na idealną receptę na aplikację, której utrzymanie będzie dość kosztowne i trudne.

Gdybyśmy rozwijali swoją aplikację bez zarządzania zależnościami doszlibyśmy do sytuacji, w której wraz ze wzrostem ilości powiązań rośnie poziom skomplikowania naszego kodu.

Nie dzwoń do nas. My zadzwonimy do Ciebie.

Nie dzwoń do nas. My zadzwonimy do Ciebie” to treść zasady Hollywood. Zasada ta określa, że poszczególne elementy naszego systemu nie powinny interesować się skąd przychodzą i dokąd idą poszczególne żądania do nich trafiające. Każdy moduł powinien skupiać się przede wszystkim na swoim zadaniu, czyli realizacji danego wywołania. Innymi słowy, klasa powinna czekać, aż otrzyma zadanie, wykonać je i tyle. Zdecydowanie nie musi wiedzieć, który moduł zlecił coś do wykonania, ani w jaki sposób zostanie to obsłużone na następnych etapach.Bezpośrednio z Zasady Hollywood możemy przejść do paradygmatu programowania, który nazywa się Odwróceniem sterowania (Inversion of Control). W klasycznym podejściu to programista ma pełną kontrolę nad wykonywanym kodem. W zasadzie sprowadza się to do sytuacji, gdy musimy na sztywno określić kolejność wywoływania poszczególnych funkcji naszego kodu, zapewnić jego spójność i przestrzegać zasad przepływu sterowania. W podejściu wykorzystującym Inversion of Control nasz program będzie składał się z wielu niezależnych od jakiekolwiek implementacji części. Porównajmy dwa poniższe przykłady:

Zwróć uwagę, że główna część naszej aplikacji (drugi przykład) nie odpowiada za kolejność transformacji i wykonywania operacji na danych wejściowych. Dopiero TaskModule ma o tym wiedzę. W pierwszym przykładzie to programista musi podejmować decyzję, kiedy które operacje się odbędą, w drugim moduł TaskModule musi to wiedzieć, reszta już nie. Może wydawać się, że w drugim przykładzie tracimy kontrolę nad tym co się dzieje w naszym kodzie, ale tak naprawdę zyskujemy coś innego - możemy skupiać się na dostarczaniu wartości biznesowych naszej aplikacji.

Wstrzykiwanie zależności

Wstrzykiwanie zależności, czyli tytułowe Dependency Injection to nic innego jak sposób wdrożenia zasad opisanych powyżej. Jest to najpopularniejsza w chwili obecnej implementacja Odwrócenia Sterowania. Błędem jest zatem utożsamianie IoC (Inversion of Control) z DI (Dependecy Injection). IoC to ogólny koncept, a DI to jego specyficzna implementacja.

Ten wzorzec projektowy oryginalnie pochodzi ze środowiska programistów Java. Ogólna zasada polega na tym, że klasa nie będzie tworzyła nowych instancji innych obiektów, które wykorzystywane będą w jej wnętrzu. Przez to nie będziemy tworzyć szczegółowych powiązań pomiędzy obiektami, a ponadto nasza klasa nie będzie musiała przekazywać żadnych parametrów do nowo tworzonego obiektu. W świecie Javy najlepiej jest operować na interfejsach klas, a nie na ich konkretnych implementacjach.

Sposobów na zastosowanie Dependecy Injection jest kilka. Jednym z nim jest wstrzykiwanie poprzez konstruktor.

Wstrzykiwanie poprzez konstruktor

Jak to wygląda w praktyce?

Zwróć uwagę, że w powyższym przykładzie nowa instancja TaskModule powstała zanim powstała instancja klasy App i została przekazana do konstruktora tej klasy. W takim podejściu nadal musimy pamiętać o zależnościach zależności i zachować odpowiednią kolejność wywołań.

W języku Java wyglądałoby to troszkę inaczej. Tutaj nie musimy wywoływać konstruktora naszej klasy TaskModule, ponieważ do klasy App przekazujemy tylko jej typ. Operowanie na typach w Javascript nie jest niestety tak proste.

Wstrzykiwanie przez Setter

Innym sposobem na wstrzykiwanie zależności do przedstawionego powyżej sposobu jest wstrzykiwanie zależności przez setter (Setter Injection). Polega to na tym, że nie przekazuje się zależności przez konstruktor, ale przez wstawienie instancji wstrzykiwanego serwisu bezpośrednio do instancji klasy, którą chcemy rozszerzyć.

Setter Injection historycznie był pierwszym sposobem na wstrzykiwanie zależności promowanym przez Spring Framework (Java). Już w 2003 roku Spring oparł na tym sposobie wstrzykiwania zależności swoje architekturę. W tamtym czasie twórcy Spring’a doszli do wniosku, że użycie wstrzykiwania poprzez konstruktor jest mniej czytelne dla programistów.

Wady i zalety

Biorąc pod uwagę fakt, że jest to implementacja Inversion of Control możemy wyróżnić podobne zalety jego stosowania, takie jak:

  • wymuszenie tworzenia kodu w małych klasach, które będą miały swoje dedykowane cele;

  • wyeliminowanie zależności pomiędzy klasami na rzecz architektury pług-in;

  • ułatwienie testowania poprzez możliwość łatwiejszego podmienienia wstrzykiwanej zależności na własną jej implementację (najczęściej zamockowaną).

Natomiast ma też swoje wady:

  • zmniejszenie czytelności kodu, gdy zależności jest zbyt dużo;

  • jeśli klasa miałaby być serializowana, wówczas będzie konieczność zdefiniowania domyślnego konstruktora;

  • przy zbyt dużej złożoności, należy przeorganizować klasę w taki sposób, aby podzielić ją na mniejsze części, aby zależności rzeczywiście były potrzebne w większości wykorzystywanych metod.

Podsumowanie

Jak widać Dependecy Injection nie jest bardzo skomplikowanym wzorcem projektowym. Często jest polecany ze względu na dużą elastyczność i łatwiejsze testowanie. Spotkać go można np. w Angularze czy Springu. Zdaję sobie sprawę, że temat nie został jeszcze wyczerpany, ale starałem się ująć go możliwie najszerzej.

Udostępnij ten artykuł:

Komentarze (0)

    Jeszcze nikt nic nie napisał, ale to znaczy że... możesz być pierwszy/pierwsza.

Powiązane treści

Jeżeli ten artykuł Cię zainteresował sprawdź inne materiały powiązane z nim tematycznie. Poniżej znajdziesz artykuły i odcinki podcastów mojego autorstwa oraz polecane przeze mnie książki, które rozszerzają ten temat.

SSR, SSG, SPA czy MPA? by Mateusz Jabłoński
Podcast
31 stycznia 2023

SSR, SSG, SPA czy MPA?

W pierwszym odcinku podcastu PiwnicaIT rozmawiamy o różnych podejściach do tworzenia aplikacji webowych. Poruszamy tematy związane z SPA, SSG, SSR czy MPA w ujęciu webdevelopmentu. Omawiamy nasze doświadczenia w pracy z różnymi bibliotekami i frameworkami dostępnymi na rynku.

Posłuchaj
Matrioszki by Frankenvrij
Artykuł
13 sierpnia 2021

Currying

Programowanie funkcyjne jest prawie tak samo popularne jak programowanie obiektowe. Wiele koncepcji z programowania obiektowego tak mocno przeniknęło do programowania w ogóle, że czasami nawet nie dostrzegamy pochodzenia danego podejścia. Programowanie funkcyjne również ma swoje ciekawe koncepcje - jedną z nich jest currying. W tym artykule staram się na przykładach pokazać jak działa currying oraz jakie problemy pozwala rozwiązać.

Czytaj więcej

Zapisz się do newslettera

Bądź na bieżąco z nowymi materiałami, ćwiczeniami i ciekawostkami ze świata IT. Dołącz do mnie.