Krytyka i wady zastrzyku uzależnienia

119

Wstrzykiwanie zależności (DI) jest dobrze znanym i modnym wzorem. Większość inżynierów zna jego zalety, takie jak:

  • Umożliwienie / łatwość izolacji podczas testów jednostkowych
  • Jawne definiowanie zależności klasy
  • Ułatwienie dobrego projektu ( na przykład zasada pojedynczej odpowiedzialności )
  • Szybkie włączanie implementacji przełączania ( DbLoggerzamiast ConsoleLoggerna przykład)

Uważam, że istnieje ogólna zgoda branży co do tego, że DI jest dobrym, użytecznym wzorcem. Obecnie nie ma za dużo krytyki. Wady wymienione w społeczności są zwykle niewielkie. Niektórzy z nich:

  • Zwiększona liczba klas
  • Tworzenie niepotrzebnych interfejsów

Obecnie rozmawiamy o projektowaniu architektury z moim kolegą. Jest dość konserwatywny, ale otwarty. Lubi kwestionować rzeczy, które uważam za dobre, ponieważ wiele osób w branży IT po prostu kopiuje najnowszy trend, powtarza zalety i generalnie nie myśli za dużo - nie analizuje zbyt głęboko.

Chciałbym zapytać o:

  • Czy powinniśmy stosować wstrzykiwanie zależności, gdy mamy tylko jedną implementację?
  • Czy powinniśmy zakazać tworzenia nowych obiektów poza językowymi / ramowymi?
  • Czy wstrzykiwanie pojedynczej implementacji jest złym pomysłem (powiedzmy, że mamy tylko jedną implementację, więc nie chcemy tworzyć „pustego” interfejsu), jeśli nie planujemy testowania jednostkowego konkretnej klasy?
Landeeyo
źródło
33
Czy naprawdę pytasz o wstrzykiwanie zależności jako wzorzec, czy pytasz o użycie frameworków DI? Są to naprawdę różne rzeczy, powinieneś wyjaśnić, którą częścią problemu jesteś zainteresowany, lub wprost zapytać o oba.
Frax
10
@Frax o wzorze, a nie frameworku
Landeeyo
10
Myląca jest inwersja zależności z wtryskiem zależności. To pierwsze jest zasadą projektowania. Ta ostatnia jest techniką (zwykle wdrażaną za pomocą istniejącego narzędzia) do konstruowania hierarchii obiektów.
jpmc26
3
Często piszę testy przy użyciu prawdziwej bazy danych i żadnych próbnych obiektów. W wielu przypadkach działa naprawdę dobrze. A wtedy przez większość czasu nie potrzebujesz interfejsów. Jeśli masz UserServicetaką klasę, to tylko uchwyt dla logiki. Dostaje połączenie z bazą danych i uruchamia testy w wycofywanej transakcji. Wielu nazwałoby to złą praktyką, ale stwierdziłem, że działa to wyjątkowo dobrze. Nie musisz przekręcać kodu tylko w celu testowania, a dostaniesz moc wykrywania błędów w testach integracyjnych.
usr
3
DI jest prawie zawsze dobry. Złą rzeczą jest to, że wiele osób myśli, że zna DI, ale wiedzą tylko, jak korzystać z dziwnych ram, nawet nie będąc pewnymi, co robią. Obecnie DI bardzo cierpi z powodu programowania kultowego.
T. Sar

Odpowiedzi:

160

Po pierwsze, chciałbym oddzielić podejście projektowe od koncepcji ram. Zastrzyk zależności na najprostszym i najbardziej podstawowym poziomie to po prostu:

Obiekt nadrzędny zapewnia wszystkie zależności wymagane od obiektu podrzędnego.

Otóż ​​to. Zauważ, że nic w tym nie wymaga interfejsów, frameworków, jakiegokolwiek stylu iniekcji itp. By być uczciwym po raz pierwszy dowiedziałem się o tym wzorze 20 lat temu. To nie jest nowe.

Z powodu zamieszania przez więcej niż 2 osoby pojęcia rodzic i dziecko w kontekście zastrzyku uzależnienia:

  • Element nadrzędny to obiekt, który tworzy instancję i konfiguruje używany obiekt podrzędny
  • Dziecko jest elementem, który jest przeznaczony do biernie instancja. Oznacza to, że jest przeznaczony do używania wszelkich zależności dostarczonych przez rodzica i nie tworzy własnych zależności.

Wstrzykiwanie zależności jest wzorem kompozycji obiektu .

Dlaczego interfejsy?

Interfejsy są umową. Istnieją po to, by ograniczać, jak ściśle mogą być połączone dwa obiekty. Nie każda zależność wymaga interfejsu, ale pomaga w pisaniu kodu modułowego.

Po dodaniu koncepcji testowania jednostkowego możesz mieć dwie implementacje pojęciowe dla dowolnego interfejsu: rzeczywisty obiekt, którego chcesz użyć w swojej aplikacji, oraz fałszywy lub pośredni obiekt, którego używasz do testowania kodu, który zależy od obiektu. Już samo to może być wystarczającym uzasadnieniem dla interfejsu.

Dlaczego frameworki?

Zasadniczo inicjowanie i zapewnianie zależności obiektom potomnym może być zniechęcające, gdy jest ich dużo. Ramy zapewniają następujące korzyści:

  • Zależności autowire od komponentów
  • Konfigurowanie komponentów z pewnego rodzaju ustawieniami
  • Automatyzacja kodu płyty kotła, abyś nie musiał widzieć go zapisanego w wielu lokalizacjach.

Mają także następujące wady:

  • Obiekt nadrzędny jest „kontenerem”, a nie niczym w twoim kodzie
  • Utrudnia to testowanie, jeśli nie można podać zależności bezpośrednio w kodzie testowym
  • Może spowolnić inicjalizację, ponieważ rozwiązuje wszystkie zależności za pomocą refleksji i wielu innych sztuczek
  • Debugowanie w czasie wykonywania może być trudniejsze, szczególnie jeśli kontener wstrzykuje proxy między interfejsem a rzeczywistym komponentem, który implementuje interfejs (przychodzi na myśl programowanie aspektowe wbudowane w Spring). Kontener jest czarną skrzynką i nie zawsze są zbudowane z myślą o ułatwieniu debugowania.

Wszystko to powiedziało, że są kompromisy. W przypadku małych projektów, w których nie ma dużo ruchomych części i nie ma powodu, aby używać frameworka DI. Jednak w przypadku bardziej skomplikowanych projektów, w których istnieją już pewne elementy, ramy można uzasadnić.

Co z [losowym artykułem w Internecie]?

Co z tym? Wiele razy ludzie mogą stać się nadgorliwi i dodawać wiele ograniczeń i krytykować cię, jeśli nie robisz rzeczy „w jeden prawdziwy sposób”. Nie ma jednego prawdziwego sposobu. Sprawdź, czy możesz wyodrębnić coś przydatnego z artykułu i zignoruj ​​rzeczy, z którymi się nie zgadzasz.

Krótko mówiąc, pomyśl sam i wypróbuj wszystko.

Praca ze „starymi głowami”

Dowiedz się jak najwięcej. Wielu programistów, którzy pracują w latach 70., odkrywa, że ​​nauczyli się nie być dogmatami w wielu sprawach. Mają metody, nad którymi pracowali od dziesięcioleci, które dają prawidłowe wyniki.

Miałem zaszczyt pracować z kilkoma z nich, a one mogą dostarczać brutalnie szczerych opinii, które mają sens. A tam, gdzie widzą wartość, dodają te narzędzia do swojego repertuaru.

Berin Loritsch
źródło
6
@CarlLeth, pracowałem z wieloma frameworkami od wersji Spring do .net. Wiosna pozwoli ci wstrzykiwać implementacje bezpośrednio na prywatne pola przy użyciu czarnej magii refleksji / obciążenia. Jedynym sposobem testowania zbudowanych w ten sposób komponentów jest użycie kontenera. Spring ma programy uruchamiające JUnit do konfigurowania środowiska testowego, ale jest to bardziej skomplikowane niż samodzielne konfigurowanie. Więc tak, podałem tylko praktyczny przykład.
Berin Loritsch
17
Jest jeszcze jedna wada, którą uważam za przeszkodę w DI poprzez frameworki, gdy mam na sobie kapelusz narzędzia do rozwiązywania problemów / opiekunów: upiorne działanie w odległości, którą zapewniają, utrudnia debugowanie offline. W najgorszym przypadku muszę uruchomić kod, aby zobaczyć, w jaki sposób inicjowane i przekazywane są zależności. Wspominasz o tym w kontekście „testowania”, ale tak naprawdę jest o wiele gorzej, jeśli zaczynasz patrzeć na źródło, nigdy umysł próbuje uruchomić go (co może wymagać mnóstwa konfiguracji). Utrudnianie mojej zdolności do odróżnienia kodu od samego spojrzenia na niego jest złą rzeczą.
Jeroen Mostert
1
Interfejsy nie są umowami, są po prostu interfejsami API. Kontrakty implikują semantykę. W tej odpowiedzi użyto terminologii specyficznej dla języka i konwencji specyficznych dla Java / C #.
Frank Hileman
2
@BerinLoritsch Głównym punktem twojej odpowiedzi jest to, że zasada DI! = Jakikolwiek dany framework DI. Fakt, że Spring potrafi robić okropne, niewybaczalne rzeczy, jest wadą Springa, a nie ogólnie ram DI. Dobry framework DI pomaga postępować zgodnie z zasadą DI bez przykrych sztuczek.
Carl Leth,
1
@CarlLeth: wszystkie frameworki DI są zaprojektowane do usuwania lub automatyzacji niektórych rzeczy, których programista nie chce przeliterować, różnią się tylko sposobem. Zgodnie z moją najlepszą wiedzą, wszystkie usuwają możliwość poznania, w jaki sposób (lub jeśli ) klasy A i B oddziałują, patrząc tylko na A i B - przynajmniej musisz także spojrzeć na konfigurację / konfigurację DI / konwencje. Nie jest to problem dla programisty (właściwie tego właśnie chcą), ale potencjalny problem dla opiekuna / debuggera (prawdopodobnie tego samego programisty, później). Jest to kompromis, którego dokonujesz, nawet jeśli Twój framework DI jest „idealny”.
Jeroen Mostert
88

Wstrzykiwanie zależności jest, podobnie jak większość wzorów, rozwiązaniem problemów . Zacznij więc od pytania, czy w ogóle masz problem. Jeśli nie, to użycie wzorca najprawdopodobniej pogorszy kod .

Najpierw zastanów się, czy możesz zmniejszyć lub wyeliminować zależności. Ponieważ wszystkie inne rzeczy są równe, chcemy, aby każdy komponent w systemie miał jak najmniej zależności. A jeśli znikną zależności, kwestia wstrzykiwania czy nie staje się dyskusyjna!

Rozważ moduł, który pobiera niektóre dane z usługi zewnętrznej, analizuje je, wykonuje skomplikowaną analizę i zapisuje wyniki w pliku.

Teraz, jeśli zależność od usługi zewnętrznej jest zakodowana na stałe, testowanie wewnętrznego przetwarzania tego modułu będzie naprawdę trudne. Dlatego możesz zdecydować się na wstrzyknięcie usługi zewnętrznej i systemu plików jako zależności interfejsu, co pozwoli ci na wstrzyknięcie próbnych rozwiązań, co z kolei umożliwi testowanie jednostkowe wewnętrznej logiki.

Ale o wiele lepszym rozwiązaniem jest po prostu oddzielenie analizy od wejścia / wyjścia. Jeśli analiza zostanie wyodrębniona do modułu bez skutków ubocznych, testowanie będzie znacznie łatwiejsze. Pamiętaj, że kpiny to zapach kodu - nie zawsze można tego uniknąć, ale ogólnie lepiej jest, jeśli możesz przetestować bez polegania na kpinach. Eliminując zależności, unikasz problemów, które DI powinien złagodzić. Zauważ, że taki projekt znacznie lepiej przylega do SRP.

Chcę podkreślić, że DI niekoniecznie ułatwia SRP lub inne dobre zasady projektowania, takie jak rozdzielenie problemów, wysoka kohezja / niskie sprzężenie i tak dalej. Równie dobrze może mieć odwrotny skutek. Rozważ klasę A, która wewnętrznie wykorzystuje inną klasę B. B jest używany tylko przez A, a zatem w pełni zamknięty i może być uważany za szczegół implementacji. Jeśli zmienisz to, aby wstrzyknąć B do konstruktora A, odsłoniłeś ten szczegół implementacji, a teraz wiedza o tej zależności i o tym, jak zainicjować B, czas życia B i tak dalej, musi istnieć jakieś inne miejsce w systemie osobno od A. Więc masz ogólnie gorszą architekturę z nieszczelnymi obawami.

Z drugiej strony istnieją przypadki, w których DI jest naprawdę przydatny. Na przykład dla usług globalnych z efektami ubocznymi, takimi jak rejestrator.

Problem polega na tym, że wzorce i architektury stają się celem samym w sobie, a nie narzędziem. Tylko pytam „Czy powinniśmy używać DI?” to rodzaj stawiania wózka przed koniem. Powinieneś zapytać: „Czy mamy problem?” i „Jakie jest najlepsze rozwiązanie tego problemu?”

Część twojego pytania sprowadza się do: „Czy powinniśmy tworzyć zbędne interfejsy, aby spełnić wymagania wzoru?” Prawdopodobnie już zdajesz sobie sprawę z odpowiedzi na to pytanie - absolutnie nie ! Każdy, kto mówi inaczej, próbuje coś ci sprzedać - najprawdopodobniej drogie godziny konsultacji. Interfejs ma wartość tylko wtedy, gdy reprezentuje abstrakcję. Interfejs, który tylko naśladuje powierzchnię pojedynczej klasy, nazywa się „interfejsem nagłówka” i jest to znany antypattern.

JacquesB
źródło
15
Nie mogłem zgodzić się więcej! Zauważ też, że kpiny ze względu na to oznaczają, że tak naprawdę nie testujemy prawdziwych implementacji. Jeśli Aużywa się go Bw produkcji, ale został przetestowany tylko w stosunku do niego MockB, nasze testy nie mówią nam, czy zadziała w produkcji. Kiedy czyste (bez efektów ubocznych) elementy modelu domeny wstrzykują się i kpią sobie, rezultatem jest ogromne marnotrawstwo czasu każdego, rozdęta i delikatna podstawa kodu oraz niska pewność wynikowego systemu. Kpij z granic systemu, a nie między dowolnymi elementami tego samego systemu.
Warbo
17
@CarlLeth Jak myślisz, dlaczego DI czyni kod „testowalnym i łatwym w utrzymaniu”, a kod bez niego mniej? JacquesB ma rację, że skutki uboczne są tym, co szkodzi testowalności / konserwacji. Jeśli kod nie ma skutków ubocznych, nie obchodzi nas, co / gdzie / kiedy / jak wywołuje inny kod; możemy zachować to w prosty i bezpośredni sposób. Jeśli kod ma skutki uboczne, możemy mieć do opieki. DI może wyciągać efekty uboczne z funkcji i umieszczać je w parametrach, czyniąc te funkcje bardziej testowalnymi, ale program bardziej złożonym. Czasami jest to nieuniknione (np. Dostęp do bazy danych). Jeśli kod nie ma skutków ubocznych, DI jest po prostu bezużyteczną złożonością.
Warbo,
13
@CarlLeth: DI to jedno rozwiązanie problemu polegającego na tym, że kod może być testowany w izolacji, jeśli ma zależności, które go zabraniają. Ale nie zmniejsza ogólnej złożoności ani nie czyni kodu bardziej czytelnym, co oznacza, że ​​niekoniecznie zwiększa łatwość konserwacji. Jeśli jednak wszystkie te zależności można wyeliminować przez lepsze rozdzielenie problemów, to doskonale „niweluje” zalety DI, ponieważ eliminuje potrzebę DI. Jest to często lepsze rozwiązanie, dzięki któremu kod jest bardziej testowalny i łatwy w utrzymaniu.
Doc Brown
5
@Warbo To było oryginalne i prawdopodobnie jedyne prawidłowe użycie szyderstwa. Nawet na granicach systemu jest rzadko potrzebny. Ludzie naprawdę marnują dużo czasu na tworzenie i aktualizowanie prawie bezwartościowych testów.
Frank Hileman
6
@CarlLeth: ok, teraz widzę, skąd bierze się nieporozumienie. Mówisz o odwróceniu zależności . Ale pytanie, ta odpowiedź i moje komentarze dotyczą DI = zastrzyk depresyjny .
Doc Brown
36

Z mojego doświadczenia wynika, że ​​istnieje wiele wad wstrzykiwania zależności.

Po pierwsze, użycie DI nie upraszcza zautomatyzowanego testowania tak bardzo, jak reklamowane. Jednostka testująca klasę za pomocą fałszywej implementacji interfejsu umożliwia sprawdzenie, w jaki sposób ta klasa będzie oddziaływać z interfejsem. Oznacza to, że pozwala ci przetestować jednostkę, w jaki sposób testowana klasa korzysta z kontraktu dostarczonego przez interfejs. Daje to jednak znacznie większą pewność, że dane wejściowe z testowanej klasy do interfejsu są zgodne z oczekiwaniami. Daje to raczej słabą pewność, że testowana klasa reaguje zgodnie z oczekiwaniami na wyjście z interfejsu, ponieważ jest to prawie ogólnie pozorne wyjście, które samo w sobie podlega błędom, nadmiernym uproszczeniom i tak dalej. Krótko mówiąc, NIE pozwala ci to sprawdzić, czy klasa będzie zachowywać się zgodnie z oczekiwaniami z rzeczywistą implementacją interfejsu.

Po drugie, DI znacznie utrudnia nawigację po kodzie. Podczas próby przejścia do definicji klas używanych jako dane wejściowe do funkcji interfejs może być czymkolwiek, od niewielkiej uciążliwości (np. W przypadku pojedynczej implementacji) do dużego pochłaniacza czasu (np. Gdy używany jest zbyt ogólny interfejs, taki jak IDisposable) podczas próby znalezienia rzeczywistej używanej implementacji. To może przekształcić proste ćwiczenie, takie jak: „Muszę naprawić wyjątek odniesienia zerowego w kodzie, który ma miejsce zaraz po wydrukowaniu tej instrukcji logowania”, w całodniowy wysiłek.

Po trzecie, użycie DI i szkieletów jest mieczem obosiecznym. Może znacznie zmniejszyć ilość kodu płyty kotłowej potrzebną do typowych operacji. Jednak dzieje się to kosztem szczegółowej wiedzy na temat konkretnego środowiska DI, aby zrozumieć, w jaki sposób te wspólne operacje są ze sobą połączone. Zrozumienie sposobu ładowania zależności do frameworka i dodanie nowej zależności do frameworka do wstrzyknięcia może wymagać przeczytania sporej ilości materiału tła i wykonania kilku podstawowych samouczków na temat frameworka. Dzięki temu niektóre proste zadania mogą być czasochłonne.

Eric
źródło
Dodałbym również, że im więcej wstrzykujesz, tym dłuższy będzie czas uruchamiania. Większość frameworków DI tworzy wszystkie instancje singletonów do wstrzykiwania w czasie uruchamiania, niezależnie od tego, gdzie są używane.
Rodney P. Barbati
7
Jeśli chcesz przetestować klasę z rzeczywistą implementacją (nie próbną), możesz napisać testy funkcjonalne - testy podobne do testów jednostkowych, ale bez użycia próbnych.
BЈовић
2
Myślę, że twój drugi akapit powinien być bardziej dopracowany: DI sam w sobie nie utrudnia (er) nawigacji po kodzie. W najprostszym przypadku DI jest po prostu konsekwencją przestrzegania SOLID. To, co zwiększa złożoność, to stosowanie niepotrzebnych struktur pośrednich i DI . Poza tym ta odpowiedź uderza w gwóźdź na głowie.
Konrad Rudolph
4
Wstrzykiwanie zależności, poza przypadkami, gdy jest to naprawdę potrzebne, jest również sygnałem ostrzegawczym, że inny zbędny kod może być obecny w dużej ilości. Kierownictwo często jest zaskoczone, gdy dowiadują się, że deweloperzy zwiększają złożoność ze względu na złożoność.
Frank Hileman
1
+1. To jest prawdziwa odpowiedź na zadane pytanie i powinna to być odpowiedź zaakceptowana.
Mason Wheeler
13

Postępowałem zgodnie z radą Marka Seemanna z „Wstrzykiwania zależności w .NET” - można się domyślać.

DI należy stosować, gdy występuje „niestabilna zależność”, np. Istnieje uzasadniona szansa, że ​​może ulec zmianie.

Więc jeśli uważasz, że możesz mieć więcej niż jedną implementację w przyszłości lub implementacja może się zmienić, użyj DI. W przeciwnym razie newjest w porządku.

Obrabować
źródło
5
Uwaga: udziela także różnych porad dotyczących OO i języków funkcjonalnych blog.ploeh.dk/2017/01/27/…
jk.
1
To dobra uwaga. Jeśli domyślnie tworzymy interfejsy dla każdej zależności, to jest to w jakiś sposób przeciw YAGNI.
Landeeyo
4
Czy możesz podać odniesienie do „Wstrzykiwanie zależności w .NET” ?
Peter Mortensen
1
Jeśli przeprowadzasz testy jednostkowe, jest wysoce prawdopodobne, że twoja zależność jest niestabilna.
Jaquez
11
Dobrą rzeczą jest to, że programiści zawsze są w stanie przewidzieć przyszłość z doskonałą dokładnością.
Frank Hileman
5

Moje największe wkurzenie na temat DI zostało już wspomniane w kilku odpowiedziach na marginesie, ale trochę o tym rozwinę. DI (jak to się dziś najczęściej robi z kontenerami itp.) Naprawdę, NAPRAWDĘ szkodzi czytelności kodu. A czytelność kodu jest prawdopodobnie przyczyną większości współczesnych innowacji programistycznych. Jak ktoś powiedział - pisanie kodu jest łatwe. Czytanie kodu jest trudne. Ale jest to również niezwykle ważne, chyba że piszesz jakieś małe narzędzie jednorazowego zapisu.

Problem z DI w tym względzie polega na tym, że jest nieprzejrzysty. Pojemnik to czarna skrzynka. Przedmioty pojawiają się skądś i nie masz pojęcia - kto je zbudował i kiedy? Co przekazano konstruktorowi? Komu udostępniam to wystąpienie? Kto wie...

A kiedy pracujesz przede wszystkim z interfejsami, wszystkie funkcje IDE idące w górę idą w górę. Okropnie trudno jest prześledzić przebieg programu bez uruchamiania go i po prostu przejścia, aby zobaczyć, KTÓRA implementacja interfejsu została użyta w tym konkretnym miejscu. Czasami pojawia się przeszkoda techniczna, która uniemożliwia przejście. I nawet jeśli możesz, jeśli wiąże się to z przechodzeniem przez skręcone wnętrzności pojemnika DI, cała sprawa szybko staje się ćwiczeniem z frustracji.

Aby efektywnie pracować z fragmentem kodu, który używał DI, musisz się z nim zapoznać i już wiedzieć, co się dzieje.

Vilx-
źródło
3

Szybkie włączanie implementacji przełączania (na przykład DbLogger zamiast ConsoleLogger)

Chociaż DI ogólnie jest z pewnością dobrą rzeczą, sugeruję, aby nie używać go na ślepo do wszystkiego. Na przykład nigdy nie wprowadzam rejestratorów. Jedną z zalet DI jest wyraźne i jasne zależności. Wymienienie ILoggerjako zależności prawie każdej klasy nie ma sensu - to tylko bałagan. Rejestrator jest odpowiedzialny za zapewnienie potrzebnej elastyczności. Wszystkie moje programy rejestrujące są statycznymi członkami końcowymi. Mogę rozważyć wprowadzenie programu rejestrującego, gdy będę potrzebował programu statycznego.

Zwiększona liczba klas

Jest to wadą danego frameworku DI lub frameworki próbnej, a nie samego DI. W większości miejsc moje klasy zależą od konkretnych klas, co oznacza, że ​​nie potrzeba żadnej płyty kotłowej. Guice (środowisko Java DI) domyślnie wiąże klasę ze sobą, a ja muszę tylko zastąpić to wiązanie w testach (lub zamiast tego połączyć je ręcznie).

Tworzenie niepotrzebnych interfejsów

Interfejsy tworzę tylko w razie potrzeby (co jest raczej rzadkie). Oznacza to, że czasami muszę zastąpić wszystkie wystąpienia klasy interfejsem, ale IDE może to dla mnie zrobić.

Czy powinniśmy stosować wstrzykiwanie zależności, gdy mamy tylko jedną implementację?

Tak, ale unikaj dodawania płyty kotłowej .

Czy powinniśmy zakazać tworzenia nowych obiektów poza językowymi / ramowymi?

Nie. Będzie wiele klas wartości (niezmiennych) i danych (mutowalnych), w których instancje zostaną po prostu utworzone i przekazane i gdzie nie ma sensu ich wstrzykiwać - ponieważ nigdy nie są przechowywane w innym obiekcie (lub tylko w innym takie obiekty).

Dla nich może być konieczne wstrzyknięcie fabryki, ale przez większość czasu nie ma to sensu (wyobraź sobie, na przykład: @Value class NamedUrl {private final String name; private final URL url;}naprawdę nie potrzebujesz tutaj fabryki i nie ma nic do wstrzyknięcia).

Czy wstrzykiwanie pojedynczej implementacji jest złym pomysłem (powiedzmy, że mamy tylko jedną implementację, więc nie chcemy tworzyć „pustego” interfejsu), jeśli nie planujemy testowania jednostkowego konkretnej klasy?

IMHO jest w porządku, o ile nie powoduje wzdęć kodu. Podaj zależność, ale nie twórz interfejsu (i żadnego szalonego config XML!), Ponieważ możesz to zrobić później bez żadnych kłopotów.

W rzeczywistości w moim obecnym projekcie są cztery klasy (spośród setek), które postanowiłem wykluczyć z DI, ponieważ są to proste klasy używane w zbyt wielu miejscach, w tym w obiektach danych.


Inną wadą większości frameworków DI jest narzut związany ze środowiskiem uruchomieniowym. Można to przenieść do czasu kompilacji (w Javie jest Dagger , nie ma pojęcia o innych językach).

Jeszcze inną wadą jest magia dziejąca się wszędzie, którą można stłumić (np. Wyłączyłem tworzenie proxy podczas korzystania z Guice).

maaartinus
źródło
-4

Muszę powiedzieć, że moim zdaniem całe pojęcie wstrzykiwania zależności jest przereklamowane.

DI jest współczesnym odpowiednikiem globalnych wartości. To, co wstrzykujesz, to globalne singletony i obiekty czystego kodu, w przeciwnym razie nie możesz ich wstrzyknąć. Większość zastosowań DI jest wymuszona na Tobie w celu korzystania z danej biblioteki (JPA, Spring Data itp.). W przeważającej części DI zapewnia idealne środowisko do pielęgnacji i uprawy spaghetti.

Szczerze mówiąc, najłatwiejszym sposobem przetestowania klasy jest upewnienie się, że wszystkie zależności są tworzone w metodzie, którą można zastąpić. Następnie utwórz klasę testową wyprowadzoną z rzeczywistej klasy i zastąp tę metodę.

Następnie tworzysz instancję klasy Test i testujesz wszystkie jej metody. Dla niektórych nie będzie to jasne - metody, które testujesz, należą do testowanej klasy. Wszystkie te testy metod odbywają się w jednym pliku klasy - klasie testów jednostkowych powiązanej z testowaną klasą. Tutaj jest zero narzutów - tak działa testowanie jednostkowe.

W kodzie ta koncepcja wygląda następująco ...

class ClassUnderTest {

   protected final ADependency;
   protected final AnotherDependency;

   // Call from a factory or use an initializer 
   public void initializeDependencies() {
      aDependency = new ADependency();
      anotherDependency = new AnotherDependency();
   }
}

class TestClassUnderTest extends ClassUnderTest {

    @Override
    public void initializeDependencies() {
      aDependency = new MockitoObject();
      anotherDependency = new MockitoObject();
    }

    // Unit tests go here...
    // Unit tests call base class methods
}

Wynik jest dokładnie równoważny z użyciem DI - to znaczy, ClassUnderTest jest skonfigurowany do testowania.

Jedyne różnice polegają na tym, że ten kod jest całkowicie zwięzły, całkowicie zamknięty, łatwiejszy do kodowania, łatwiejszy do zrozumienia, szybszy, zużywa mniej pamięci, nie wymaga innej konfiguracji, nie wymaga żadnych ram, nigdy nie będzie przyczyną 4 stron (WTF!) Ślad stosu, który zawiera dokładnie napisane przez Ciebie klasy ZERO (0) i jest całkowicie oczywisty dla każdego, kto ma choćby najmniejszą wiedzę OO, od początkującego do Guru (można by pomyśleć, ale pomyliłby się).

Biorąc to pod uwagę, oczywiście nie możemy go użyć - jest to zbyt oczywiste i niewystarczająco modne.

Ostatecznie jednak moim największym zmartwieniem w przypadku DI jest to, że projekty, które widziałem nieszczęśliwie, zakończyły się niepowodzeniem, wszystkie z nich były ogromnymi bazami kodu, gdzie DI był klejem łączącym wszystko. DI nie jest architekturą - jest tak naprawdę istotne tylko w kilku sytuacjach, z których większość jest zmuszana do korzystania z innej biblioteki (JPA, Spring Data itp.). W przeważającej części, w dobrze zaprojektowanej bazie kodu, większość zastosowań DI występuje na poziomie poniżej, gdzie mają miejsce codzienne działania programistyczne.

Rodney P. Barbati
źródło
6
Opisałeś nie ekwiwalent, ale przeciwieństwo wstrzykiwania zależności. W twoim modelu każdy obiekt musi znać konkretną implementację wszystkich swoich zależności, ale używając DI, który staje się obowiązkiem „głównego” komponentu - skleić odpowiednie implementacje razem. Pamiętaj, że DI idzie w parze z innym odwracaniem zależności DI, w którym nie chcesz, aby komponenty wysokiego poziomu miały silne zależności od komponentów niskiego poziomu.
Logan Pickup
1
dobrze, gdy masz tylko jeden poziom dziedziczenia klas i jeden poziom zależności. Z pewnością zamieni się w piekło na ziemi, gdy się rozszerzy?
Ewan
4
jeśli użyjesz interfejsów i przeniesiesz initializeDependencies () do konstruktora, to samo, ale ładniejsze. Następny krok, dodanie parametrów konstrukcyjnych oznacza, że ​​możesz zlikwidować wszystkie swoje TestClasses.
Ewan
5
Jest w tym tyle złego. Jak powiedzieli inni, twój przykład „odpowiednika DI” wcale nie jest wstrzykiwaniem zależności, jest antytezą i pokazuje całkowity brak zrozumienia tej koncepcji oraz wprowadza inne potencjalne pułapki: częściowo zainicjowane obiekty mają zapach kodu Jak Ewan sugeruje: Przenieś inicjalizację do konstruktora i przekaż je za pomocą parametrów konstruktora. Więc masz DI ...
Mr.Mindor
3
Dodanie do @ Mr.Mindor: istnieje bardziej ogólne „sekwencyjne sprzężenie” anty-wzorcowe, które nie dotyczy tylko inicjalizacji. Jeśli metody obiektu (lub, bardziej ogólnie, wywołania interfejsu API) muszą być uruchamiane w określonej kolejności, np. barMożna je wywołać dopiero później foo, to jest to zły interfejs API. To twierdząc , aby zapewnić funkcjonalność ( bar), ale nie możemy rzeczywiście go używać (ponieważ foonie mogły być nazywane). Jeśli chcesz pozostać przy swoim initializeDependencies(anty?) Wzorcu, powinieneś przynajmniej ustawić go na prywatny / chroniony i wywołać go automatycznie z konstruktora, aby interfejs API był szczery.
Warbo,
-6

Naprawdę twoje pytanie sprowadza się do „Czy testowanie jednostkowe jest złe?”

99% alternatywnych klas do wstrzyknięcia będzie próbkami umożliwiającymi testowanie jednostkowe.

Jeśli wykonujesz testy jednostkowe bez DI, masz problem z tym, jak sprawić, by klasy korzystały z fałszywych danych lub fałszywej usługi. Powiedzmy „część logiki”, ponieważ być może nie dzielisz jej na usługi.

Są na to alternatywne sposoby, ale DI jest dobry i elastyczny. Gdy masz go już do przetestowania, jesteś praktycznie zmuszony do używania go wszędzie, ponieważ potrzebujesz jakiegoś innego kodu, nawet tak zwanego „DI biedaka”, który tworzy konkretne typy.

Trudno wyobrazić sobie taką wadę, że przewaga testów jednostkowych jest przytłoczona.

Ewan
źródło
13
Nie zgadzam się z twoim twierdzeniem, że DI dotyczy testów jednostkowych. Ułatwienie testów jednostkowych jest tylko jedną z zalet DI i prawdopodobnie nie jest najważniejszą.
Robert Harvey
5
Nie zgadzam się z twoim założeniem, że testy jednostkowe i DI są tak blisko. Korzystając z makiety / kodu pośredniczącego, sprawiamy, że zestaw testów jeszcze bardziej nas okłamuje: testowany system wychodzi dalej od prawdziwego systemu. To obiektywnie złe. Czasami przewyższa to pozytywnie: wyśmiewane połączenia FS nie wymagają czyszczenia; kpione żądania HTTP są szybkie, deterministyczne i działają w trybie offline; itd. Natomiast za każdym razem, gdy używamy kodu na newstałe w metodzie, wiemy, że ten sam kod działający w środowisku produkcyjnym działał podczas testów.
Warbo
8
Nie, to nie „czy testowanie jednostkowe jest złe?”, To „czy kpina (a) jest naprawdę konieczna i (b) warta wzrostu złożoności?” To jest zupełnie inne pytanie. Testowanie jednostkowe nie jest złe (dosłownie nikt się o to nie kłóci) i zdecydowanie jest tego warte. Ale nie wszystkie testy jednostkowe wymagają drwiny, a drwina wiąże się z dużymi kosztami, dlatego należy przynajmniej rozsądnie z niej korzystać.
Konrad Rudolph
6
@Ewan Po twoim ostatnim komentarzu nie sądzę, że się zgadzamy. Mówię, że większość testów jednostkowych nie potrzebuje DI [frameworki] , ponieważ większość testów jednostkowych nie wymaga kpiny. W rzeczywistości użyłbym tego nawet jako heurystyki dla jakości kodu: jeśli większość twojego kodu nie może być testowana jednostkowo bez obiektów DI / mock, to napisałeś zły kod, który był zbyt poprawnie sprzężony. Większość kodu powinna być w dużym stopniu oddzielona od siebie, mieć pojedynczą odpowiedzialność i ogólne przeznaczenie, a także być trywialnie testowalna w izolacji.
Konrad Rudolph
5
@Ewan Twój link zawiera dobrą definicję testów jednostkowych. Według tej definicji moim Orderprzykładem jest test jednostkowy: testuje metodę ( totalmetodę Order). Narzekasz, że to wywołuje kod z 2 klas, moja odpowiedź brzmi: co ? Nie testujemy „2 klas jednocześnie”, testujemy totalmetodę. Nie powinno nas obchodzić, w jaki sposób metoda spełnia swoje zadanie: są to szczegóły dotyczące implementacji; ich testowanie powoduje kruchość, ścisłe sprzężenie itp. Dbamy tylko o zachowanie metody (zwracana wartość i skutki uboczne), a nie o klasy / moduły / rejestry procesora / itp. wykorzystano w tym procesie.
Warbo