IoC - kiedy jest już tego za dużo?

0

Temat bazuje na Javie, ale dotyczy w zasadzie dowolnego IoC.

Mam taki kejs:
Standardowy (powiedzmy) projekt z użyciem DI / IoC (Java 8, Spring 4).
Klasa A (1k LOC, serwis) ma 16 dependencji, wrzucanych przez konstruktor.
Chcę dorzucić użycie klasy B typu "utils" (90 LOC, jedna instancja wystarczy), ale takiej która zależy od niektórych dependencji klasy A.
Mogę po prostu dodać 17 dependencję. Ale coś mnie skręca żeby tego nie robić, zwłaszcza że klasa B jest bardzo prosta.
Wrzucam ją w ciele konstruktora przez "new" - też nie za dobrze. Zaraz się ktoś przyczepi że nie może tego zmockować.

Jakie są alternatywy?
Kiedy wg Was klasa jest za prosta na wstrzykiwanie przez @Autowired i oznaczanie przez @Service / @Component?
Bo na razie widzę, że adnotacje @Service / @Component automatycznie rozprzestrzeniają się na wszystkie klasy jak COVID-19.

Czytałem:
https://softwareengineering.stackexchange.com/questions/231867/which-classes-should-be-autowired-by-spring-when-to-use-dependency-injection
https://stackoverflow.com/questions/49988942/spring-is-using-new-a-bad-practice
ale nic przekonującego nie znalazłem.

Macie jakieś namiary na jakieś "best practice" nie-używania IoC w IoC?
Załóżmy, że w projekcie nadal chcę korzystać z kontenera IoC (Spring).

Link dla ludzi nie obeznanych ze Springiem: https://www.baeldung.com/spring-autowire

@jarekr000000 ?

4

Nigdy nie używam kontenera do DI, bo to głupie :-) Przecież to łatwa droga do zrobienia sobie klasy z 16 zależnościami.
(tzw. "miara spier....nia springowego"). Klasa która ma więcej niż 5 zależności jest już raczej zrypana i robi za dużo/ (za dużo wie). Są czasem uzasadnione wyjątki oczywiście.

Jeśli mam soft z jakieś powodu w Springu i nie chce orać architektury to zestawiam springową "wydmuszkę". Kontrolery springowe są zarządzane przez spring. Wstawiam w nie też wszelkie zależności od zasobów (zwykle tylko jakiś datasource lub persisencecontext). A na boku buduje już po ludzku normalne klasy i przekazuję sterowanie do nich, wszystko jak człowiek przez new`.

Zresztą normalnie odbywa się to w takiej kolejności: buduję cały moduł bez żadnych springów - z pełnymi testami i dopiero na sam koniec podłączam do Springowej fasady (kontroler). Oczywiście w moim module DI jest - po prostu na konstruktorach i bez frameworków.

Kilka projektów tak mam zrobionych i ogólnie ludzie byli zadowoleni - głównie przez to, że testy chodzą bardzo szybko bez całego narzutu springa, a jednocześnie są to testy używające bazy danych i pokazują faktyczne problemy. Oczywiście czasem ktoś nowy się dziwił - jest już całe pokolenie spring juniorów - tego się łatwo nie wyleczy.

Ortogonalną kwestią jest czy w ogóle używać w niektórych przypadkach DI. Jeśli masz klasę, która ma jedną instancję i nie przeszkadza w testach (bo nie odwołuje się do zewnętrznych zasobów) to zwykle takiej klasy nie przekazuję jako dependency. Acha! baza danych to często nie jest zewnętrzny zasób - przeważnie nie jest.

3
vpiotr napisał(a):

Standardowy (powiedzmy) projekt z użyciem DI / IoC (Java 8, Spring 4).

No, dla mnie niestandardowy. Udaje mi się pracować w Javie poza Springiem

Klasa A (1k LOC, serwis) ma 16 dependencji, wrzucanych przez konstruktor.

Nie wiem czy powiem coś odkrywczego ale ładne rzeźbienie w brązie. Myślałem że jestem szambonurkiem bo dostałem legacy gdzie standard to 6 zależności w klasie, ale widzę że tobie udało się dostać coś jeszcze lepszego.

Co można doradzić? Ciąć, dzielić i zszywać ponownie czyli ogólnie refaktoryzować aż liczba zależności dla pojedynczej klasy się zmniejszy :(

6

5 zależności to maks mniej więcej jaki powinien być. Niestety każdy framework IoC powoduje że łatwo ten limit przekroczyć.
A klasy z logika biznesowa nie powinny mieć żadnych @Component, @Service itp.
Ani żadnych zależności do Springa czy jakiegokolwiek frameworka.

1

@Aleksander32: zgadza się, ale to ile powinno być zależności to mi Sonar mówi (ogólnie to dużo rzeczy mówi, ale nikt go nie słucha).

1

W Twoim wypadku odpowiedź brzmi: klasa nie jest nigdy na to za mała jeśli nie jest za mała żeby w ogóle istnieć. NIe ma sensu używać mieszanego podejścia:
0 korzyści.
Problemy:
Największy - "skąd się to tam bulwa mać bierze? pół godziny już IoC konfig przeglądam i nie ma Jechana magia springa? WTF?!" Brak spójności. Więcej WTF w kodzie. Ludzie będą się gubić jeśli coś tak standardowego robi się raz tak a raz inaczej. Bez sensu. DO tego ktoś się zacznie zastanawiać co jest w niej takiego specjalnego i czy cały system się nie zawali, jeśli przenioą ją do IoC i mogą się bać ruszyć w ogóle. W końcu ta jedna klasa jest tak specjalnie zrobiona.... kolejny WTF.

Mniejsze: problemy z testowaniem, problemy jeśli klasa urośnie, problemy jeśli więcej zależności trzeba do niej będzie dodać. Problemy przy code review i statycznej analizie - nie widać rzeczywistych zależności danej klasy która tej małej używa.

Co do innej dyskusji - Można używać albo i nie kontenerów,(zwłaszcza w bibliotekach kontenery IoC mogą powodować problemy). Ja używam. Jest ciekawa prezentacja na temat zalet nieużywania IoC, może uda mi się ją znaleźć później...

4

Ja się zgodzę z przedmówcami wyżej, jak dla mnie problem leży gdzieś między klasa ma 1k LOC a klasa ma 16 zależności przez konstruktor ;)

Jesteś w stanie jakoś rozbić tego molocha na mniejsze kawałki?

0

@superdurszlak:

superdurszlak napisał(a):

Jesteś w stanie jakoś rozbić tego molocha na mniejsze kawałki?

Jestem, ale to jedna z prostszych / mniejszych klas (taki dispatcher) :)
Mała wartość dodana refaktoringu. Chociaż może trzeba o tym pomyśleć.

3
Aleksander32 napisał(a):

5 zależności to maks mniej więcej jaki powinien być. Niestety każdy framework IoC powoduje że łatwo ten limit przekroczyć.

Ja się zasadniczo nie zgodzę, że jest to wina narzędzia. To nie noże zabijają.

2
  1. Ja osobiście zacząłbym od pozbycia się @Service i @Autowired i zrobienia klasy @Configuration, tak zeby mieć jasne miejsce gdzie obiekty są tworzone i potem nie zastanawiać się skąd się bierze X.
  2. Nie robiłbym jakichś cudów z serii 5 zależności jest ok ale 6 już nie bo to zależy od twojego przypadku. Zerkam teraz w kod nad którym pracuje i mam klasę która ma 6 zależności i 70 linii kodu. Czemu tyle zależności? Bo akurat ta klasa składa informacje od 6 "providerów". Można by pewnie coś zgrupować robiąc jakiś sztuczny byt ale nie widzę specjalnie sensu. U ciebie widać że jednak tego kodu jest dużo i na 99% da się to podzielić na kawałki. Obawiam się, że to nie IoC jest tu problemem ;)
0

No to musicie zlikwidować większość prelegentów na konferencjach i ich repo na githubie :)

2

W kotlinie można całkiem ładnie zrobić dependency injection dzięki open val i lazy.
Z zabawkowego projektu.
Odpowiednik @Configuration czy też Module z guice. Nie wymaga żadnych frameworków.

open class StonesModule(
    private val infra: InfrastuctureModule) {

    private val seq: DbSequence by lazy {
        DbSequence(infra.context, Sequences.GLOBALSTONESSEQ)
    }

    open val stoneRepo by lazy { StoneRepo(infra.context, seq) }

    open val stoneService by lazy { StoneService(infra.context, stoneRepo) }

}

potem można sobie zrobić w teście:

val testContext =  object: StonesModule(testInfra) {
   override val stoneRepo = DurneRepo
}

Da się coś zbliżonego w javie, ale nie jest tak elastycznie - korzystałem nawr z Lazy vavr do tego. Ale brak open val to trochę kłopot.

0
jarekr000000 napisał(a):

W kotlinie można całkiem ładnie zrobić dependency injection dzięki open val i lazy.

Trochę dziwny język, skoro trzeba do tego jakiejś specjalnej składni. Zazwyczaj wystarcza konstruktor.


Co do tematu - z moich skromnych obserwacji wynika, że są dwa powody, dla których klasa ma przerost zależności:

  1. Jest multifasadą, tzn. ma kilka publicznych metod, każda z nich ma kilka zazwyczaj różnych zależności, ale ktoś wpadł na pomysł, żeby wszystko wrzucić do jednej klasy, bo np. w każdej metodzie jest używany jakiś logger. Czyli np. metoda A ma zależności: 1,2,3; metoda B:1,3,4,5,6; metoda C: 1,6,7,8, itd.
  2. Jest efektem nierozumienia czym jest jednostka, czyli tak naprawdę zależności jest pięć, ale ktoś podąża za kultem "każda klasa to jednostka", więc internalowe utilsy wstrzykuje do klasy jako zależności, zamiast je po prostu tworzyć tam, gdzie są potrzebne.

Pierwsze jest łatwiej rozwiązać, drugie nieco trudniej, bo wymaga usuwania zbędnych interfejsów, mocków i testów, ale też jest wykonalne.

1 użytkowników online, w tym zalogowanych: 0, gości: 1