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 Foo
komponuje 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 Foo
niezależ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 .