W poprzednich postach i na moim githubie pokazałem jak można obejść się bez frameworku DI w przykładowym problemie postawionym przez @Shalom. Ta praca, się jeszcze nie skończyła – bo zamierzam ten zabawny projekt pociągnąć dalej.
Myślę jednak, że w zasadzie mogę odpowiedzieć na postawione zagadnienie - DI , Testy itp.
Tylko uwaga długie :-(
Na wstępie:
Jestem silnym przeciwnikiem frameworków DI - typu Spring, Guice lub JavaEE (olewając fakt, że JavaEE to nie miał być framework DI :P ).
Uważam, że injectiony (@Autowired, @Inject, @ejb czy niejawne) to takie GOTO naszych czasów i trzeba to zwalczać.
Dlaczego:
- Niepotrzebne/ bez sensu
Po pierwsze w większości przypadków nie są potrzebne. Jest to zbędna "magia" dorzucana do kodu, która powoduje, że nawet jeśli kod się kompiluje i przechodzi wszystkie unit testy ... to nadal może nie działać (bo np. ktoś coś zmienił w konfigu - wstrzyknięcia nie są jednoznaczne etc.).
Jeżeli jakaś klasa, którą obrażę - nazywając ją czasowo Beanem, ma tylko jedną implementację w systemie - wówczas z pewnością żadne wstrzykiwanie nie jest potrzebne - bo przecież wiadomo co tworzymy i można użyć 'new'... (Pomijam tutaj testowe instancje - ten problem ogarnę osobno).
1.1 Zarażeni
I tu podnosi się krzyk, a co jak ta instancjonowana klasa ma jakieś Injectiony! Przecież, po użyciu 'new', nie zainstancjonują się!
Dokładnie, nie zainstancjonują się - i tu widać pierwszą dużą wadę frameworków DI - są jak wirus HIV - jak jeden Bean się zarazi, to wszystkie współpracujące automatycznie
muszą ulec zarażeniu i system w sposób spektakularny zmienia się w wielką wylęgarnię HIV. (A przecież, jak wszyscy będą zarażeni to już problemu nie będzie...).
(Ten akapit nie ma na celu dyskryminować nikogo z HIV – potrzebowałem, jako przykład, wybrać znaną chorobę, która roznosi się między innymi przez stosunki płciowe).
1.2 Multum zależności
No, a co z klasami, gdzie jest 5-10 zależności - będziesz to wszystko instancjonował ręcznie? Otóż odpowiedź jest prosta -
takie klasy to właśnie objaw zarażenia DI HIV-em. W normalnych warunkach po dwóch, trzech zależnościach zapala się czerwona lampka i programista przemyśliwuje design (no dobra - tak jeszcze nie jest, ale się poprawia).
Mając framework DI ... można sprawę olać - coś tam się wstrzyknie i będzie działać! To jest super ! I jeśli walczymy o szybki prototyp takie podejście może się sprawdzić.
Natomiast próba dłuższego utrzymania takiego kodu to niestety koszmar. Testowanie, choćby nie wiem jak sprytne były Mocki,jest albo nieefektywne (testujemy Mockito), albo bardzo żmudne.
Najgorsze, że jak się odpowiednio w takie bagno wdepnie - to powstaje system opleciony gigantyczną siecią zależności miedzy klasami, których każde zerwanie (refaktoring) boli.
Jako, że mam na utrzymaniu kilka systemów napisanych w JavaEE oraz Springu - widzę ile czasu spędzam z zespołem na odkrywaniu dlaczego coś mi się tu nie wstrzyknęło (zwykle jest tak po każdym refaktoringu). Czy muszę pisać, że Mocki dla odmiany wstrzykują się świetnie i testy przechodzą bez problemu....
1.3 Inicjalizacja/ Startup
No, a co z inicjalizacją systemu - taki framework DI potrafi bardzo ładnie poukładać kolejność startowania elementów i nie trzeba się przejmować, całą tą siatką połączeń.
W zasadzie jest to ciekawy argument, oznacza, że pogodzenie się, z faktem nie panowania nad systemem....
Z drugiej strony jako „anarchitekt”, paru systemów przyznaje, że akurat totalnie panowanie nad systemem nie jest tak ważne - ważne jest aby system działał!
I tu po pierwsze: są lepsze rozwiązania - jak np. lazy val w Scali. W przypadku Javy - mamy różne patterny, które to robią (http://www.theserverside.com/news/1321145/Programmatic-Dependency-Injection-with-an-Abstract-Factory – choć akurat to taki sobie artykuł).
Prawdziwy problem frameworków DI to niestety: nieprzewidywalne zachowanie. Wystarczy jedno przepakietowanie i może okazać się, że połowa systemu nie wstaje.
Bo jest zupełnie inna kolejność inicjalizacji!
Posiadanie systemu, który wstaje w magiczny sposób wcale nie jest dobrym rozwiązaniem na dłuższą metę.
Dorobiłem się już nawet jednego systemu, który zawsze źle wstaje (z Exceptionami)... - i nikt juz nie wie jak go rozplątać (tu akurat zrobił swoje OSGi).
1.4 Magia
To jest w sumie najważniejszy argument. Jak mówił Greg Young - "try to explain dynamic proxy to junior". Istniejące frameworki, ze względów technicznych mają czasem zabawne ograniczenia. Piękne katastrofy dzieją się jak się napisze - return this; albo odwoła do metody z tego samego beana:
this.callAnotherMethodThatNeedsSomeInterceptorLikeForInstanceTransactionHandling
... i nagle nie działają interceptory. Ja wiem, jak takie frameworki działają i generalnie nie mam z tym problemów (oprócz śmiechu, przez łzy ile razy tworze sztucznego beana, bo to żeby sztucznie przedelegowąć wywołanie metody do innego, i interceptory miały szansę).
Ale TO JUŻ NIE JEST JAVA... to jest java z magią, bo zwykłej Javie return this nie jest niebezpieczny!
I naprawdę, nie tylko juniorzy wpadają w takie pułapki, to jest po prostu nieintuicyjne, więc raz na jakiś czas każdy wpada. (Np. po refaktoringu).
1.5 A może jednak
To kiedy warto używać frameworka DI ?
W zasadzie jest jeden rozsądny przypadek - nasz system wspiera pewną koncepcje pluginów/wtyczek i chcemy sobie na etapie budowania systemu, albo nawet runtime
dynamicznie podmieniać implementacje. Jedna implementacja dla klienta A, inna dla B. Wówczas taki framework jest niesamowicie wygodny ... o ile tylko ograniczy się jego zasięg rażenia. Kilka, kilkanaście klas funkcjonujących jako beany - i jest super!
Kiedy ostatnio taki system widziałem... (w dobie mikroserwisów)... już nie pamiętam.
- Testy
Testy - to jest najbardziej tajemniczy przypadek. Nie wiem jakim cudem, ale szczególnie Springowi udało się sprzedać, jako idealny framework do testów. Wystarczy, na czas testów zrobic cały testowy kontekst, z testową bazą danych, innymi implementacjami niektórych beanów, a całe środowisko, wszystko się samo wstrzyknie i życie (testy) będzie piękne.
2.1 Oh really?
Pytanie - numer 1, kto korzystając ze springa faktycznie tak robi? Jeszcze parę lat temu to się zdarzało, ale obecnie większość przypadków to Unit testy - i mockito!
A do tego, żaden testowy kontekst nie jest potrzebny. Co więc przynosi taka praktyka ? - testy które testują Mockito!
Dlatego, że podnoszenie na czas testów kontekstu Springowegom czy JEE jest w istocie męczące, więc omija się problem i wrzuca wszędzie Mockito. (I unit testy działają bez Springa....).
2.2 Mockito
Najgorsze, że pozór wydaje się, że Mockito to doskonała oszczędność czasu - wystarczy napisać Mockito.when(...) coś tam, coś tam i mock gotowy. To, co jednak w takim podejściu umyka,
to fakt, że mnóstwo takich 'whenów' się powtarza... potem powstaje system, gdzie po każdym refaktoringu trzeba przeryć połowę testów. A co komu po testach, które trzeba od razu zmieniać
z implementacją - w czym one pomagają przy refaktoringu?
2.3 Jak więc?
Jakie jest prawidłowe rozwiązanie -> otóż:
- odpalanie unitu bez mocków, dokładnie tak jak działa (i wtedy testuje się naprawdę to co działa)!,
- ręczne pisanie mocków stubów, które potem można wielokrotnie używać,
W zasadzie jeżeli pisze się jakiś większy, cięższy serwis, to warto od razu implementować jego "Mock" (to poniekąd cześć dokumentacji), tak aby innym się łatwo testowało. ( Choć, w praktyce, rzadko to robię...).
Jeśli natomiast jakaś klasa: szybko się startuje, nie zależy od zewnętrznych zasobów... to niby dlaczego utrudniać sobie życie i ją mokować?
2.4 A może ….
I znowu jest przypadek kiedy Mockito jest jak najbardziej słuszne - kiedy odwołujemy się do zasobów systemowych, lub kiedy rozmawiamy z serwisami zewnętrznymi, które ktoś inny napisał.
Wówczas możemy się uniezależnić od ich implementacji i spokojnie posymulować ich odpowiedzi Mockitem. Ale to jest zwykle kilka, kilkanaście takich na aplikację.
Natomiast używanie Mockito, do własnych klas ... to najczęściej choroba -> HIV.
2.5 Testy integracyjne
Ciekawa sprawa te tzw. testy integracyjne, osobiście uważam, że to pojęcie w zasadzie sztuczne. W projektach Spring, JavaEE, OSGI szczególnie to widać.
Testy integracjne to u typowego Javowego zespołu - testy komponentów na kontenerze, gdzie można sprawdzić czy wszystko się po-wstrzykiwało.....
Czyli ich jedynym sensem istnienia, jet fakt, że używany framework DI jest niegodny zaufania i nie można polegać na Unit testach, bo wszystko może rypnąć przy realnym starcie.
Brawo Spring! Naprawdę ułatwia... (przy okazji artykuł (lekko poza tematem): http://blog.thecodewhisperer.com/permalink/integrated-tests-are-a-scam)
-
Inne nałogi
Jakie jeszcze inne choroby przynoszą frameworki DI -
Chyba najgorszy problem to tzw. ContextBean. W wielu dużych projektach, zespoły dorabiają się patternu, gdzie prawie każdy komponent ma wstrzyknięte coś co nazywa się
ContextBean - i jest tam: konfiguracja systemu, imię, nazwisko i numer buta użytkownika, prognoza pogody na następne pięć minut etc..
Najfajniej jest jak ktoś tam jeszcze wrzuci HashMapę, która zawierać może wszystko... Jest to problem spowodowany specyficznym czasem życia Beanów np. springowych, które zupełnie nie pasują do czasu cyklu życia funkcji. Więc programiści głupieją, zamiast np. tworzyć sobie Parameter Object , Buildera czy tym podobne.
-
To nie wina frameworku
Ale przecież! - Te patologie powyżej świadczą tylko, że zespół jest kiepski, a nie framework.
To jest argument typu - komunizm był dobry , tylko ludzie za kiepscy. Zupełnie podobne pojawiały się jak Dijkstra pisał "GOTO considered harmful".
Pewnie! Jak zespół bedzie trzymał dyscyplinę i pilnował każdej linijki kodu - to problemu nie będzie.
Praktyka, jest jednak taka, że jeżeli środowisko pracy jest jak pole minowe i trzeba dużo czasu spędzać na "patrolowanie" (review), to jest to środowisko do bani.
Lepsze byłoby środowisko, w którym pisanie utrzymywanego (pewnego! )kodu jest promowane! Będzie wychodziło - niejako przypadkiem, bo robienie skrótów będzie od razu boleć.
- Po co się mieszam?
A teraz ode mnie - dlaczego mi tak zależy?
Jakby nie patrzeć zarabiam na życie zwalczając problemy JavaEE, Spring itp. , bo przeczytałem kiedyś te specyfikacje, znam dobrze javę, proxy, classloadery, jvm...
Spoko.
Problem polega na tym, że naprawdę mam już wymioty jak widzę, jak kolejny zespół Javowy tworzy potwora na miarę: "EnterpriseFizzBuzz" (https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition), z którego jest naprawdę dumny.
Kilkaset klas, siedem warstw, osiem frameworków i realizuje funkcjonalność, którą dwa chłopaki od NodeJS robią w 8 godzin (z przerwą na grę w CS)...
Bo tamci podchodzą do zagadnienia tak:
- co jest do zrobienia,
- jak to najprościej - rozsądnie zrobić,
- robimy.
Typowy zespół javowy: nie wie jeszcze co będzie robić, nie ma nawet projektu - a już się kłóci Spring czy JavaEE, a może Guice, JPA czy Hibernate, Oracle czy MySQL..., a potem stara się wszędzie powciskać te technologie (żeby udowodnić, że technologie działają i wybór był słuszny).
Co by tu jeszcze wstrzyknąć?....