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 (
DbLogger
zamiastConsoleLogger
na 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?
UserService
taką 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.Odpowiedzi:
Po pierwsze, chciałbym oddzielić podejście projektowe od koncepcji ram. Zastrzyk zależności na najprostszym i najbardziej podstawowym poziomie to po prostu:
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:
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:
Mają także następujące wady:
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.
źródło
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.
źródło
A
używa się goB
w produkcji, ale został przetestowany tylko w stosunku do niegoMockB
, 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.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.
źródło
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
new
jest w porządku.źródło
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.
źródło
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
ILogger
jako 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.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).
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ć.
Tak, ale unikaj dodawania płyty kotłowej .
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).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).
źródło
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 ...
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.
źródło
bar
Można je wywołać dopiero późniejfoo
, to jest to zły interfejs API. To twierdząc , aby zapewnić funkcjonalność (bar
), ale nie możemy rzeczywiście go używać (ponieważfoo
nie mogły być nazywane). Jeśli chcesz pozostać przy swoiminitializeDependencies
(anty?) Wzorcu, powinieneś przynajmniej ustawić go na prywatny / chroniony i wywołać go automatycznie z konstruktora, aby interfejs API był szczery.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.
źródło
new
stałe w metodzie, wiemy, że ten sam kod działający w środowisku produkcyjnym działał podczas testów.Order
przykładem jest test jednostkowy: testuje metodę (total
metodęOrder
). Narzekasz, że to wywołuje kod z 2 klas, moja odpowiedź brzmi: co ? Nie testujemy „2 klas jednocześnie”, testujemytotal
metodę. 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.