Średnio raz na miesiąc słyszę od kogoś o "zaawansowanych technikach" typu Dependency Injection. Ktoś opowiada, że jakiś język ma wstrzykiwanie zależności a inny go nie ma. Ktoś opowiada, że gdzieś się da to zastosować, a gdzieś indziej się nie da. Zazwyczaj wtedy w mojej głowie pojawiają się pytania "Ohh, shit - czy ja o czymś nie wiem? Czyli jednak wstrzykiwanie zależności jest dużo bardziej zaawansowanym konceptem, niż mi się pierwotnie wydawało?". Szybka odpowiedź - nie jest.

Dzisiaj  w TypeScripcie, bo jego mam pod ręką.

Czym jest wstrzykiwanie zależności?

Załóżmy, że jesteśmy w miarę doświadczonymi programistami i nie tworzymy jednej wielkiej klasy implementującej wszystko co się da. Zamiast tego tworzymy kilka mniejszych, współpracujących ze sobą, klas.

Przykład bez wstrzykiwania zależności.

W powyższym przykładzie nie wstrzykujemy żadnej zależności. Klasa Controller tworzy instancję klasy MongodbNews (która jest tzw. serwisem), a następnie wykorzystuje ją do utworzenia Newsa po naciśnięciu przycisku.

Magiczne Dependency Injection

Wstrzykiwanie zależności będziemy mieć, wówczas gdy instancję MongodbNews dostarczymy klasie Controller z zewnątrz. Po prostu. To jest to sławne wstrzykiwanie zależności.

Wstrzykiwanie zależności (nieidealne).

Nie ma różnicy, czy taką instancję klasy wstrzykniemy przez:

  • konstruktor,
  • funkcję init(),
  • za pomocą settera,
  • pole publiczne.

Wymogiem jest tylko to, że musimy instancję innej klasy zapodać z zewnątrz.

Wstrzykiwanie zależności vs odwracanie zależności

W jednym ze swoich postów Reguły biznesowe w... gamedevie? pisałem o fundamentach czystej architektury. Wspomniałem tam, że komponenty (przyjmijmy klasy) nie powinny zależeć od komponentów niższego poziomu. Najbardziej oczywistym symptomem takiej zależności jest sytuacja, gdy komponent wykorzystuje gdzieś nazwę klasy innego komponentu.

Klasa A zależy od klasy B.

Litera D w zasadach SOLID

Cóż mamy zrobić gdy potrzebujemy takiej zależności przy jednoczesnym zachowaniu czystej architektury? Odwrócić zależność - Dependency Inversion!

Wystarczy, że w komponencie "zażądamy" określonego zestawu funkcji tworząc interfejs. Klasy komponentu niższego poziomu powinny ten interfejs implementować.

Klasa A zależy od interfejsu I. Klasa B implementuje interfejs I.

Offtopic: Moje zdanie o "programowaniu do interfejsów"

Może się mylę, może w przyszłości zmienię zdanie - ale moim zdaniem interfejsy najczęściej nie są potrzebne. Wiele osób wpada w praktykę ukrywania wszystkiego za interfejsami, myśląc, że to automatycznie tworzy czystą architekturę. Jestem pewien, że każda z tych osób zna i potrafi rozszyfrować skrót SOLID. Jednak zastanawia mnie, w jaki sposób te osoby to interpretują.

Tworzenie tak dużej ilości zbędnych kawałków kodu może sprawić, że jakiekolwiek przyszłe zmiany w kodzie będą utrudnione. Przed utworzeniem interfejsu powinniśmy pomyśleć o takim grafie zależności i sprawdzić czy na pewno chcemy zmienić typ zależności. Innym powodem może być implementowanie jakiegoś wzorca i chęć utrzymania pomiędzy klasami jakiejś konsekwencji.

Największym chyba niezrozumieniem konceptu zależności jest stawianie interfejsu w klasach, które są ze sobą w relacji 1:1 (jedna z klas istnieje tylko dla drugiej). Relacja 1:1 charakteryzuje się tym, że równie dobrze kod jednej z klas mógłby się znajdować w tej drugiej. Przykładem takiego podziału jest wzorzec MVVM, gdzie klasa ViewModel jest wydzielonym kodem z View. Stawianie między nimi interfejsu, jest tylko proszeniem się o dodatkową pracę.

Ale wróćmy do tematu posta.

Ładniejsze wstrzykiwanie zależności

Największą wadą zaprezentowanego wcześniej rozwiązania wykorzystującego DI, jest fakt, że klasa Controller dokonuje wyborów implementacji (użycie bazy MongoDB), co nie powinno należeć do jej obowiązków. Powiedzieliśmy sobie również, że nie powinna ona również zależeć od tak uszczegółowionej klasy, ponieważ jest ona niższego poziomu.
To idealne miejsce, aby zastosować interfejs. Świadomie!

Dependency Injection

Mały hint: wprawne oko może zauważyć, że poprzez odpowiednie decyzje doszliśmy do wzorca projektowego o nazwie Strategia.

"Mój język nie ma DI"

Zakładając, że ktoś mówi o języku obiektowym - naprawdę trudno trafić na taki język, który nie pozwalałby na wstrzykiwanie zależności. Wszystkim tym ludziom prawdopodobnie chodzi o brak kontenera DI.

Kontener DI, to klasa która przechowuje wszystkie "wybory" zależności i dostarcza je w razie takiej potrzeby. Poprzednio zaprezentowane implementacje miały jedną wadę - wymagały każdorazowego tworzenia obiektu zależności aby go następnie wstrzyknąć. Zamiast używać Singletonów dla serwisów, nasz kontener DI będzie zawierał w sobie instancje zależności i nie będzie potrzeby ich powtórnego konkretyzowania.

Dependency Injection Container.

Niektóre frameworki (jak np. Symphony/Laravel, Angular, NestJS) kontrolują moment tworzenia obiektów swoich klas, aby na podstawie typów argumentów konstruktora wstrzyknąć potrzebną zależność. Proces ten jest tak wygodny i niewidoczny dla programisty wykorzystującego framework, że często programista ulega wrażeniu, że jest to funkcja środowiska programistycznego aniżeli zwykłego kodu wykorzystującego refleksję.

Podsumowując

Wstrzykiwanie zależności to naprawdę prosta operacja a nie żaden skomplikowany system. DI i świadome stawianie interfejsów trochę same tworzą czystą architekturę.
Ten post będzie taką moją tarczą. Link do niego będę wysyłał każdemu, kto znów mnie zestresuje swoim spojrzeniem na DI :)

Dla zainteresowanych polecam lekturę Czysta architektura. Struktura i design oprogramowania. Przewodnik dla profesjonalistów:

czysta-architektura-struktura-i-design-oprogramowania-przewodnik-dla-profesjonalistow-robert-c-martin,czarch