Dependency Injection: z kontenerem czy bez?

16

Dependency Injection to bardzo potężny i przydatny wzorzec projektowy. Pozwala zaprowadzić w kodzie porządek, jawnie zadeklarować powiązania między klasami i uprościć proces utrzymania kodu. Powstała cała masa narzędzi wspomagających nas w tym światłym dziele. I po co?

Akcja: BLOGvember! Post nr 17.
W listopadzie w każdy roboczy poranek na devstyle.pl znajdziesz nowy, świeżutki tekst. W sam raz do porannej kawy na dobry początek dnia.
Miłej lektury i do przeczytania jutro! :)

Kontenery

Kontenery Dependency Injection powstały, aby uprościć proces tworzenia obiektów w systemie. Mówimy im, jakie klasy wchodzą w skład systemu, a one sobie wszystkie klocki układają. Zależności między strukturami wykrywają. Drzewa zasadzają, grafy modelują. By na koniec: tworzyć instancje.

Co bardziej zaawansowane kontenery potrafią o wiele więcej. A to Aspect-Oriented Programming nam udostępnią, a to dekoratory w prosty sposób pozwolą dorzucić, a to Dispose() wywołają kiedy trzeba, a to to, a to tamto. Więcej można poczytać w poście Profesjonalne kontenery i DI: kontener.

Lata temu ludzie rzucili się na tę rodzinę narzędzi. Powstawały coraz to nowe, bardziej skomplikowane. Później, jak to często bywa, nastąpił odwrót i tendencja upraszczania całego procesu zarządzania zależnościami. Aż dotarliśmy do stanu równowagi: equilibrium. Wszyscy byli szczęśliwi.

A potem: z dev-Olimpu poleciały gromy.

Alternatywa

Po co nam dodatkowe narzędzia? Możemy to naklepać ręcznie! W każdym projekcie z osobna! Bo kontenery są trudne! I powinniśmy minimalizować liczbę zależności w projekcie!

Takie oto głosy zaczęły dobiegać z programistycznych niebiesiech. Okazało się, że na przykład Mark Seemann (autor książki “Dependency Injection in .NET”) i Greg Young (kto nie zna, ten niech pozna we własnym zakresie) wiele mają kontenerom do zarzucenia. Że to UNIKALNE (IUnikable) zło. Że niepotrzebne. Że skomplikowane.

Polecam chociażby te linki:

Argumenty przedstawiane w tych treściach na pewno nie są bezpodstawne. Ci kolesie, jak już otwierają paszczę lub siadają do klawiatury, to wiedzą co czynią. Mam tylko jeden problem: zaprezentowane remedium… zupełnie do mnie nie przemawia.

Eksperymentalnie zrealizowałem kiedyś projekt bez wykorzystania kontenera. Wszystkie zależności rzeźbiłem ręcznie. Projekt ten miał raptem kilkadziesiąt klas na krzyż (“las krzyży” vs “las klas”?), a i tak kod budujący obiekty wyglądał bardzo… kupiaszczo. Nie chciałbym teraz do niego wrócić.

Werdykt

Kontenery nie są narzędziami banalnymi w “obsłudze”. Ich niepoprawne skonfigurowanie może nieść za sobą katastrofalne konsekwencje. Co powiedz na aplikację, która wysypuje się co 2 tygodnie, bo z powodu nieprzemyślanych rejestracji obiekty mnożą się jak wirusy i zżerają całą pamięć? Wiesz, ile czasu może zająć diagnostyka i profilowanie? Ja wiem :), więc podpowiem: DNI całe!

Ale z drugiej strony: popatrz na kod w podlinkowanych postach i prezentacjach.

Lambda na lambdzie i lambdą pogania. Poukrywane, pozagnieżdżane. Ja osobiście wolę mieć 50 linii czytelnego kodu niż 8 linii kodu, który bez problemu “załapie” maksymalnie 10% populacji.

Albo IFy, SWITCHe. Kurde… ControllerFactory o nazwie PoorMansCompositionRoot? Z mega-switchem? Ciut nie pattern-matching, tylko dla BARDZO ubogich.

Bardzo szanuję obu Panów, są półbogami programowania. Gdyby ich scalić, to powstałby dev-bóg. Ale tego… po prostu nie kupuję.

Oczywiście, każda zależność, referencja, to dodatkowy koszt. Każde narzędzie to proces nauki, obowiązki związane z utrzymaniem i aktualizacjami. Każda biblioteka to ryzyko “cudzych” błędów. Ale czy to powód, żeby wszystko pisać samemu?

NIE! A nawet: NIH! NIH Syndrome się kłania. Tak, jak bez sensu jest pisać własny kontener (co na jutubie czynię :) ), tak bez sensu jest ręcznie, samodzielnie, dbać o wszystkie rejestracje i tworzenie obiektów.

Z głową i umiarem. Świadomie, panie i panowie. Ze zrozumieniem, bez tępego copy/paste ze StackOverflow. Oto uniwersalna porada i maść na ból wszelaki.

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

16 Comments

  1. Albo wziąć język funkcyjny, gdzie nie ma obiektów i już :) Ale fakt tu już pewnie tylko 3% populacji zrozumie “o co kaman” :)

  2. Zajmowałem się problemem wycieków pamięci w aplikacji webowej – spring+scala+akka. Scenariusz podobny do opisanego przez Ciebie – apka się wykładała średnio co 3-4 tygodnie. Diagnoza? – OutOfMemoryError. Przyczyna? heh… Debugowanie języka funkcyjnego tudzież scali z akką to była istna mordęga… Skoki między tonami klas, masa apply’jów i problemy z zatrzymywaniem się w breakpointach… Nie wspominając już o tych wszystkich skrócikach i “uproszczeniach” konstrukcji, które do mnie nie trafiają. Jak słyszę teraz o tym, że ktoś się zachwyca scalą, jej skalowalnością, lambdami i że jest taka wow – mam ochotę strzelać. Scala może sama w sobie nie jest aż taka zła, niechęć wynika z problemów z debuggowaniem. Może dlatego, że miałem starszą wersję IDE(późny 2014), chociaż z wersją scali wydaną w końcu 2013 roku powinien sobie radzić. Niemniej jednak, niesmak pozostał ;) A wracając do przyczyny – przyczyną był mechanizm dead-letters w akka, który nadawał wiadomości do endpointa, który nie istniał, bo został nieprawidłowo odrejestrowany przez framework przy utracie połączenia i nie dawało się go już ponownie zarejestrować, pomimo usilnych prób. A martwe wiadomości składowane były w pamięci przez akkę. Dzięki, ulżyło mi ;) Co do samego DI, bo małego offtopa trzasnąłem – jak dla mnie przydatny mechanizm/wzorzec i nie zgodzę się z Panami, którzy twierdzą, że jest to zło. “Narzędzie” jak każde inne, w niektórych przypadkach będzie miało lepsze zastosowanie w innych gorsze. Ważne jest, by szyć rozwiązania na miarę, a nie według jednego utartego schematu – “bo w projekcie X taka architektura była i było git”. Tak jak wspomniałeś – z głową i z umiarem :)

    • Bartek,
      Też swego czasu wieele czasu spędziłem na szukaniu błędu, a okazało się że wszystkie informacje siedziły sobie w dead letters queue. Nie akka co prawda, ale ból pewnie podobny.

      Podeślij mi to co napisałeś w rozszerzonej formie i wrzucimy w cykl Wasze Historie, co Ty na to? :).

      Jeszcze gwoli ścisłości: Panowie nie twierdzą, że DI to zło. Tylko że kontenery są zbędne. I czasami pewnie tak, ale jakbym miał generalizować, to bym raczej generalizował w drugą stronę.

  3. Również tego nie kupuję, zarówno bez sensu jest bezmyślne ładowanie do projektu miliona frameworków i bibliotek jak i pisane siłę swoich rozwiązań, które ktoś już zrobił dobrze i dopracował, bo siedzi w temacie od lat. Wyobraźmy sobie stolarza który zamiast robić zlecenie, zajmuje się produkcją narzędzi bo “zrobi to lepiej i uniezależni się od producentów”.

  4. – Sądzę jednak, że łatwiej jest ludziom wyjaśnić jak działają funkcje anonimowe / lambdy, niż tłumaczyć API do biblioteki mającej prawie tyle kodu, co projekt do którego jej używam.
    – Używanie kontenera zasadniczo przenosi cię w strefę programowania dynamicznego: skąd wiesz, że poprawnie podpiąłeś wszystkie zależności i twój program działa? Cóż, nie dowiesz się tego po prostu kompilując swój projekt. Musisz go odpalić i wywołać kod w każdym miejscu gdzie używasz DI. Oczywiście możesz napisać do tego testy, tylko że to mija się z początkowym założeniem.
    – Zauważyłem, że nagminnym problemem w projektach korzystających z DI jest tendencja ludzi do korzystania z wzorca God Object: “Mamy mało czasu, nie będę wydzielał tej funkcjonalności do nowego kontrolera, wystarczy że dorzucę 30ty parametr do konstruktora, który już istnieje i reszta sama się zrobi”. Spotkałem się z takim podejściem w co najmniej kilku firmach.

    Szczerze, z moich obserwacji wynika, że to właśnie stosowanie kontenerów DI wymaga właśnie zdyscyplinowanego i “kumatego” zespołu. W przeciwnym razie projekt zaczyna gnić w bardzo szybkim tempie. Sam nie korzystam z DI od roku i nie widzę żadnego spadku wydajności w swojej pracy.

    Widzę za to, że nie mam problemów ze ściganiem źle rozwiązanych zależności, które czasami ciężko jest wychwycić przed wrzuceniem aplikacji na serwer testowy, czasem nawet na produkcję (Przykład: źle rozwiązane connection stringi, które “u mnie działały” ponieważ wszystkie prowadziły do tej samego testowego serwera).

    • Bartosz,
      API biblioteki kontenera to zwykle Register() i Resolve(), czego zresztą i tak się nie dotyka podczas normalnej pracy.
      A skąd wiem, że “będzie działać”? Bo napiszę testy oczywiście! Więcej o tym za kilka dni.
      Problem z rozbudowanymi konstruktorami i wieloma zależnościami: zgadzam się, to zła droga. Chociaż nie w kontenerach upatruję tutaj wrogów, a raczej w “automocking containers”, które w testach potrafią jedną linijką zbudować dowolny obiekt, niezależnie od jego skomplikowania. Tych nie używam, bo taki scenariusz MA boleć.

      Mamy różnie doświadczenia, co może wynikać ze specyfiki projektów i zespołu :). Ja pracowałem albo sam albo w małych zespołach (chociaż projekty były spore i skomplikowane). Raz zrobiłem (niewielki) projekt bez kontenera, i więcej tego nie powtórzę. Przynajmniej nie w takich samych warunkach (OOP, mvc).

      A błąd ze źle skonfigurowanymi connection stringami zrzucać na kontener DI to… trochę takie nie bardzo, moim zdaniem :). Błędy się zdarzają i zawsze będą się zdarzać, szczególnie takie, które wynikają z nieuwagi.

      • Mój problem z connection strings wynikał ze specyfiki i faktu, że jeden interfejs może mieć wiele implementacji (kto w czasach kontenerów IoC pomyślałby o podobnej herezji :) ), które muszą być podpinane zależnie od przypadku.

        Zabawne jest to, że uważamy, że ręczna konstrukcja obiektów jest uciążliwa, ale nie mamy nic przeciwko ręcznej rejestracji komponentów, ręcznemu pisaniu, utrzymywaniu i uruchamianiu testów, a potem ręcznemu grzebaniu się przez 40 linijek stack trace żeby zobaczyć co w tym wszystkim było nie tak.

        • Nie rozumiem co mają kontenery do wielu implementacji interfejsu :).
          Ręczna rejestracja jest czasami konieczna, ale w przypadku kontenera będzie prostsza i krótsza niż tworzenie ręcznie każdej instancji każdej klasy. Tylko trzeba wiedzieć co się robi – w obu przypadkach.
          Testy: co kto lubi, ja lubię :). I one naprawdę eliminują wiele problemów związanych z kontenerami.
          Przebijanie się przez stack trace będzie podobnie skomplikowane i z kontenerem i bez.

    • Krzysztof Skrzynecki on

      Istnieją również kontenery, które sprawdzają zależności podczas kompilacji – Dagger (https://google.github.io/dagger/) jest dobrym przykładem. Jestem ciekaw czy istnieją podobnę rozwiązania dla świata poza javowego

  5. Wojciech on

    Zysk z weryfikacji w trakcie kompilacji bije wszystko pozostałe na głowe. Jeśli mamy framework, który umie zrobić DI w czasie kompilacji (np. macwire w scali) to spoko. W pozostałych wypadkach, sorry memory.

  6. Pingback: Dependency Injection potrafi... zaskoczyć. Historia pewnego Singletona. | devstyle.pl | Maciej Aniserowicz

  7. IMHO, kontenery (nikt nie mówi o IoC jako takim) powinny być dodawane do projektów w kolejnej fazie. To zrobiło się tak bardzo popularne, że nawet nikt nie zauważa wrzucania kontenerów (z mojego podwórka nawet tak prostych jak Guice) do 3 klasowej applikacji jako zwykły błąd i overengineering. Czy nie przypomina to mikroserwisów? W przypadku kontenerów bardzo ciężko wyznaczyć granicę, kiedy “narzut” się opłaca, a kiedy nie – bo właściwie to kwestia developera jaki styl pracy mu się bardziej podoba. Pomijam fakt, że po prostu można nie potrafić programować bez użycia kontenerów – co też się zdarza.