Ile kosztuje za duże wstrzyknięcie zależności?

38

Pracuję w projekcie wykorzystującym (Spring) Dependency Injection do dosłownie wszystkiego, co jest zależnością klasy. Jesteśmy w punkcie, w którym plik konfiguracyjny Spring urósł do około 4000 linii. Niedawno obejrzałem jedną z przemówień wuja Boba na YouTube (niestety nie mogłem znaleźć linku), w których zaleca wstrzyknięcie tylko kilku głównych zależności (np. Fabryk, bazy danych itp.) Do głównego komponentu, z którego następnie być dystrybuowanym.

Zaletą tego podejścia jest oddzielenie frameworka DI od większości aplikacji, a także sprawia, że ​​konfiguracja Spring jest czystsza, ponieważ fabryki będą zawierały znacznie więcej tego, co było w konfiguracji wcześniej. Przeciwnie, spowoduje to rozpowszechnienie logiki tworzenia wśród wielu klas fabrycznych, a testowanie może stać się trudniejsze.

Więc moje pytanie naprawdę brzmi, jakie inne zalety lub wady widzisz w jednym lub drugim podejściu. Czy są jakieś najlepsze praktyki? Wielkie dzięki za odpowiedzi!

Antymon
źródło
3
Posiadanie pliku konfiguracyjnego 4000 linii nie jest winą wstrzyknięcia zależności ... istnieje wiele sposobów, aby to naprawić: zmodularyzuj plik na wiele mniejszych plików, przełącz się na zastrzyk oparty na adnotacjach, użyj plików JavaConfig zamiast xml. Zasadniczo problemem jest brak zarządzania złożonością i rozmiarem potrzeb wstrzykiwania zależności aplikacji.
Maybe_Factor
1
@Maybe_Factor TL; DR podzielił projekt na mniejsze komponenty.
Walfrat

Odpowiedzi:

44

Jak zawsze It Depends ™. Odpowiedź zależy od problemu, który próbuje się rozwiązać. W tej odpowiedzi postaram się odnieść do niektórych wspólnych sił motywujących:

Preferuj mniejsze bazy kodu

Jeśli masz 4000 wierszy kodu konfiguracyjnego Spring, przypuszczam, że baza kodu zawiera tysiące klas.

Nie jest to problem, który można rozwiązać po fakcie, ale z reguły preferuję mniejsze aplikacje z mniejszymi bazami kodu. Jeśli zajmujesz się projektowaniem opartym na domenie , możesz na przykład stworzyć bazę kodu dla określonego kontekstu.

Opieram tę radę na moim ograniczonym doświadczeniu, ponieważ przez większość mojej kariery pisałem internetowy kod firmy. Mogę sobie wyobrazić, że jeśli tworzysz aplikację komputerową, system osadzony lub inny, trudniej jest rozdzielić.

Chociaż zdaję sobie sprawę, że ta pierwsza rada jest z pewnością najmniej praktyczna, uważam również, że jest najważniejsza i dlatego ją uwzględniam. Złożoność kodu zmienia się nieliniowo (możliwie wykładniczo) wraz z rozmiarem podstawy kodu.

Faworyzuj Pure DI

Chociaż wciąż zdaję sobie sprawę, że to pytanie przedstawia istniejącą sytuację, zalecam Pure DI . Nie używaj kontenera DI, ale jeśli tak, przynajmniej użyj go do implementacji kompozycji opartej na konwencjach .

Nie mam żadnego praktycznego doświadczenia ze Springem, ale zakładam, że z pliku konfiguracyjnego wynika plik XML.

Konfiguracja zależności za pomocą XML jest najgorszym z obu światów. Po pierwsze, tracisz bezpieczeństwo podczas kompilacji, ale nic nie zyskujesz. Plik konfiguracyjny XML może łatwo być tak duży, jak kod, który próbuje zastąpić.

W porównaniu do problemu, jaki ma rozwiązać, pliki konfiguracyjne wstrzykiwania zależności zajmują niewłaściwe miejsce w zegarze złożoności konfiguracji .

Przypadek wtrysku gruboziarnistej zależności

Mogę uzasadnić zastrzyk gruboziarnistej zależności. Mogę również uzasadnić iniekcję drobnoziarnistej zależności (patrz następny rozdział).

Jeśli wstrzykniesz tylko kilka „centralnych” zależności, wówczas większość klas może wyglądać następująco:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Jest to nadal pasuje do wzorców projektowych „s przysługę obiektu kompozycję nad dziedziczenie klasy , bo Fookomponuje Bar. Z punktu widzenia łatwości utrzymania, nadal można to uznać za możliwe do utrzymania, ponieważ jeśli chcesz zmienić skład, po prostu edytuj kod źródłowy Foo.

Nie jest to wcale trudniejsze do utrzymania niż wstrzykiwanie zależności. W rzeczywistości powiedziałbym, że łatwiej jest bezpośrednio edytować klasę, która używa Bar, zamiast podążać za pośrednią właściwością wstrzykiwania zależności.

W pierwszym wydaniu mojej książki o Dependency Injection rozróżniam zależności niestabilne i stabilne.

Niestabilne zależności to te zależności, które należy rozważyć wstrzyknięcie. Zawierają

  • Zależności, które należy ponownie skonfigurować po kompilacji
  • Zależności opracowane równolegle przez inny zespół
  • Zależności z zachowaniem niedeterministycznym lub zachowanie ze skutkami ubocznymi

Z drugiej strony stabilne zależności to zależności, które zachowują się w dobrze zdefiniowany sposób. W pewnym sensie można argumentować, że to rozróżnienie uzasadnia zastrzyk gruboziarnistej zależności, chociaż muszę przyznać, że nie do końca zdawałem sobie z tego sprawę, kiedy pisałem książkę.

Jednak z perspektywy testowania utrudnia to testowanie jednostkowe. Nie można już testować jednostkowo Fooniezależnie Bar. Jak wyjaśnia JB Rainsberger , testy integracyjne cierpią z powodu kombinatorycznej eksplozji złożoności. Musisz dosłownie napisać dziesiątki tysięcy przypadków testowych, jeśli chcesz objąć wszystkie ścieżki poprzez integrację nawet 4-5 klas.

Kontrargumentem jest to, że często twoim zadaniem nie jest programowanie klasy. Twoim zadaniem jest opracowanie systemu, który rozwiązuje określone problemy. Taka jest motywacja rozwoju opartego na zachowaniach (BDD).

Inny pogląd na to przedstawia DHH, który twierdzi, że TDD prowadzi do uszkodzenia projektowego wywołanego testem . Preferuje również gruboziarniste testy integracyjne.

Jeśli weźmiesz tę perspektywę na rozwój oprogramowania, to zastrzyk gruboziarnistej zależności ma sens.

Przypadek iniekcji drobnoziarnistej zależności

Z drugiej strony wstrzyknięcie zależności drobnoziarnistej można opisać jako wstrzyknięcie wszystkich rzeczy!

Moją główną troską dotyczącą wstrzykiwania gruboziarnistej zależności jest krytyka wyrażona przez JB Rainsbergera. Nie można pokryć wszystkich ścieżek kodu testami integracyjnymi, ponieważ trzeba napisać dosłownie tysiące lub dziesiątki tysięcy przypadków testowych, aby pokryć wszystkie ścieżki kodu.

Zwolennicy BDD przeciwstawią się argumentowi, że nie musisz obejmować wszystkich ścieżek kodu testami. Musisz pokryć tylko te, które wytwarzają wartość biznesową.

Z mojego doświadczenia wynika jednak, że wszystkie „egzotyczne” ścieżki kodu będą również wykonywane przy wdrożeniu na dużą skalę, a jeśli nie zostaną przetestowane, wiele z nich będzie mieć wady i powodować wyjątki w czasie wykonywania (często wyjątki o zerowym odwołaniu).

To spowodowało, że faworyzuję wstrzykiwanie drobnoziarnistych zależności, ponieważ pozwala mi testować niezmienniki wszystkich obiektów w izolacji.

Preferuj programowanie funkcjonalne

Podczas gdy ja skłaniam się w kierunku drobnoziarnistego wstrzykiwania zależności, przesunąłem swój nacisk na programowanie funkcjonalne, między innymi dlatego, że jest ono wewnętrznie testowalne .

Im bardziej zbliżasz się do kodu SOLID, tym bardziej staje się on funkcjonalny . Wcześniej czy później możesz równie dobrze się zanurzyć. Architektura funkcjonalna to architektura portów i adapterów , a wstrzykiwanie zależności jest również próbą portów i adapterów . Różnica polega jednak na tym, że język taki jak Haskell wymusza tę architekturę poprzez swój system typów.

Preferuj statyczne programowanie funkcjonalne

W tym momencie zasadniczo zrezygnowałem z programowania obiektowego (OOP), chociaż wiele problemów związanych z OOP jest wewnętrznie związanych z głównymi językami, takimi jak Java i C #, niż sama koncepcja.

Problem z głównymi językami OOP polega na tym, że prawie niemożliwe jest uniknięcie problemu kombinatorycznej eksplozji, który - niesprawdzony - prowadzi do wyjątków w czasie wykonywania. Z kolei języki o typie statycznym, takie jak Haskell i F #, umożliwiają kodowanie wielu punktów decyzyjnych w systemie pisma. Oznacza to, że zamiast pisać tysiące testów, kompilator po prostu powie ci, czy poradziłeś sobie ze wszystkimi możliwymi ścieżkami kodu (do pewnego stopnia; to nie jest srebrna kula).

Ponadto wstrzykiwanie zależności nie działa . Prawdziwe programowanie funkcjonalne musi odrzucić całe pojęcie zależności . Rezultatem jest prostszy kod.

Podsumowanie

Jeśli jestem zmuszony do pracy z C #, wolę precyzyjne wstrzykiwanie zależności, ponieważ pozwala mi to pokryć całą bazę kodu możliwą do zarządzania liczbą przypadków testowych.

Ostatecznie moją motywacją jest szybkie sprzężenie zwrotne. Mimo to testy jednostkowe nie są jedynym sposobem uzyskania informacji zwrotnej .

Mark Seemann
źródło
1
Naprawdę podoba mi się ta odpowiedź, a zwłaszcza ta instrukcja używana w kontekście Springa: „Konfiguracja zależności za pomocą XML jest najgorsza z obu światów. Po pierwsze, tracisz bezpieczeństwo podczas kompilacji, ale nic nie zyskujesz. Konfiguracja XML plik może być tak duży, jak kod, który próbuje zastąpić. ”
Thomas Carlisle,
@ThomasCarlisle nie było celem konfiguracji XML, że nie musisz dotykać kodu (nawet kompilować), aby go zmienić? Nigdy go (lub ledwo) używałem z powodu dwóch rzeczy wymienionych przez Marka, ale zyskujesz coś w zamian.
El Mac
1

Ogromne klasy konfiguracji DI są problemem. Ale pomyśl o kodzie, który zastępuje.

Musisz utworzyć wszystkie te usługi i tak dalej. Kod, który by to zrobił, albo znajdowałby się w app.main()punkcie początkowym i byłby wstrzykiwany ręcznie, albo ściśle powiązany, jak this.myService = new MyService();w ramach klas.

Zmniejsz rozmiar klasy konfiguracji, dzieląc ją na wiele klas konfiguracji i wywołuj je od punktu początkowego programu. to znaczy.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

Nie musisz odwoływać się do usługi 1 lub 2, z wyjątkiem przekazania ich do innych metod konfiguracji ci.

Jest to lepsze niż próba pobrania ich z kontenera, ponieważ wymusza kolejność wywoływania ustawień.

Ewan
źródło
1
Dla tych, którzy chcą dokładniej przyjrzeć się tej koncepcji (budowanie drzewa klas w punkcie początkowym programu), najlepszym terminem do wyszukania jest „
rootowanie
@Ewan co to za cizmienna?
superjos
Twoja klasa konfiguracji ci z metodami statycznymi
Ewan
urg powinien być di. ciągle myślę o „kontenerze”
Ewan