Ramy wstrzykiwania zależności, takie jak Google Guice, dają następującą motywację do ich użycia ( źródło ):
Aby zbudować obiekt, najpierw zbuduj jego zależności. Ale aby zbudować każdą zależność, potrzebujesz jej i tak dalej. Więc kiedy budujesz obiekt, naprawdę musisz zbudować wykres obiektu.
Ręczne budowanie wykresów obiektów jest pracochłonne (...) i utrudnia testowanie.
Ale nie kupuję tego argumentu: nawet bez struktury wstrzykiwania zależności mogę pisać klasy, które są łatwe do utworzenia i wygodne do przetestowania. Na przykład przykład ze strony motywacyjnej Guice można przepisać w następujący sposób:
class BillingService
{
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
// constructor for tests, taking all collaborators as parameters
BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
this.processor = processor;
this.transactionLog = transactionLog;
}
// constructor for production, calling the (productive) constructors of the collaborators
public BillingService()
{
this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
{
...
}
}
Mogą więc istnieć inne argumenty za szkieletem wstrzykiwania zależności ( które nie mieszczą się w tym pytaniu !), Ale łatwe tworzenie grafów obiektowych do testowania nie jest jednym z nich, prawda?
new ShippingService()
.Odpowiedzi:
Trwa stara, stara debata na temat najlepszego sposobu wstrzykiwania zależności.
Pierwotne cięcie sprężyny tworzyło prosty obiekt, a następnie wprowadzało zależności za pomocą metod ustawiających.
Ale potem duża grupa ludzi nalegała, aby wstrzykiwanie zależności za pomocą parametrów konstruktora było właściwym sposobem na zrobienie tego.
W ostatnim czasie, gdy stosowanie refleksji stało się bardziej powszechne, ustalanie wartości członków prywatnych bezpośrednio, bez seterów i argumentów konstruktorów, stało się wściekłością.
Twój pierwszy konstruktor jest więc zgodny z drugim podejściem do wstrzykiwania zależności. To pozwala robić fajne rzeczy, takie jak wstrzykiwanie próbnych testów.
Ale konstruktor bez argumentów ma ten problem. Ponieważ tworzy instancję klas implementacji dla
PaypalCreditCardProcessor
iDatabaseTransactionLog
, tworzy twardą zależność od PayPal i bazy danych w czasie kompilacji. Bierze odpowiedzialność za prawidłowe zbudowanie i skonfigurowanie całego drzewa zależności.Wyobraź sobie, że procesor PayPay jest naprawdę skomplikowanym podsystemem i dodatkowo pobiera wiele bibliotek pomocniczych. Tworząc zależność czasu kompilacji od tej klasy implementacji, tworzysz niezniszczalny link do całego drzewa zależności. Złożoność wykresu obiektowego właśnie wzrosła o rząd wielkości, może dwa.
Wiele z tych elementów w drzewie zależności będzie przezroczystych, ale wiele z nich będzie również wymagać utworzenia. Szanse są, nie będziesz mógł po prostu utworzyć wystąpienia
PaypalCreditCardProcessor
.Oprócz tworzenia wystąpień każdy obiekt będzie wymagał zastosowania właściwości z konfiguracji.
Jeśli masz tylko zależność od interfejsu i zezwalasz na zbudowanie i wstrzyknięcie zależności przez zewnętrzną fabrykę, odcinają one całe drzewo zależności PayPal, a złożoność kodu zatrzymuje się na interfejsie.
Istnieją inne korzyści, takie jak określenie klas implementacji w konfiguracji (tj. W czasie wykonywania zamiast kompilacji) lub bardziej dynamiczna specyfikacja zależności, która różni się, powiedzmy, w zależności od środowiska (test, integracja, produkcja).
Załóżmy na przykład, że procesor PayPal miał 3 obiekty zależne, a każda z tych zależności miała dwa kolejne. Wszystkie te obiekty muszą pobrać właściwości z konfiguracji. Kod w stanie, w jakim się znajduje, wziąłby na siebie odpowiedzialność za zbudowanie tego wszystkiego, ustawienie właściwości z konfiguracji itp. Itd. - wszystkie obawy, którymi zajmie się środowisko DI.
Na początku może nie wydawać się oczywiste, przed czym się osłaniasz za pomocą frameworka DI, ale z czasem sumuje się i staje się boleśnie oczywiste. (lol mówię z doświadczenia, które próbowałem zrobić to na własnej skórze)
...
W praktyce, nawet w przypadku bardzo małego programu, kończę pisanie w stylu DI i rozkładam klasy na pary implementacyjne / fabryczne. To znaczy, jeśli nie używam frameworka DI, takiego jak Spring, po prostu łączę kilka prostych klas fabrycznych.
Zapewnia to rozdzielenie obaw, aby moja klasa mogła to zrobić, a klasa fabryczna bierze na siebie odpowiedzialność za budowanie i konfigurowanie rzeczy.
Nie jest to wymagane podejście, ale FWIW
...
Mówiąc bardziej ogólnie, wzorzec interfejsu DI / zmniejsza złożoność kodu, wykonując dwie czynności:
wyodrębnianie zależności końcowych na interfejsy
„podnoszenie” zależności upstream z kodu do pewnego rodzaju kontenera
Ponadto, ponieważ tworzenie instancji i konfiguracja obiektów jest dość znanym zadaniem, środowisko DI może osiągnąć wiele korzyści skali dzięki znormalizowanemu zapisowi i zastosowaniu sztuczek takich jak odbicie. Rozproszenie tych samych obaw wokół klas powoduje w końcu więcej bałaganu, niż mogłoby się wydawać.
źródło
Wiążąc wykres obiektów na wczesnym etapie procesu, tj. Podłączając go do kodu, potrzebujesz zarówno kontraktu, jak i implementacji. Jeśli ktoś (może nawet Ty) chce użyć tego kodu w nieco innym celu, musi ponownie obliczyć cały wykres obiektu i ponownie go wdrożyć.
Frameworki DI pozwalają pobrać kilka komponentów i połączyć je ze sobą w czasie wykonywania. To sprawia, że system jest „modułowy”, składający się z szeregu modułów, które działają wzajemnie na interfejsy zamiast na ich implementacje.
źródło
Podejście „tworzenie instancji moich własnych współpracowników” może działać w przypadku drzew zależności , ale z pewnością nie zadziała dobrze w przypadku wykresów zależności, które są ogólnie kierowanymi wykresami acyklicznymi (DAG). W zależności DAG wiele węzłów może wskazywać na ten sam węzeł - co oznacza, że dwa obiekty używają tego samego obiektu jako współpracownika. Tego przypadku nie można w rzeczywistości skonstruować przy użyciu podejścia opisanego w pytaniu.
Jeśli niektórzy z moich współpracowników (lub współpracowników współpracujących) powinni udostępnić określony obiekt, musiałbym utworzyć instancję tego obiektu i przekazać go moim współpracownikom. Tak naprawdę musiałbym wiedzieć więcej niż moi bezpośredni współpracownicy, a to oczywiście nie skaluje się.
źródło
UnitOfWork
który ma jednoDbContext
i wiele repozytoriów. Wszystkie te repozytoria powinny używać tego samegoDbContext
obiektu. Dzięki zaproponowanemu przez OP „autodetekcji” staje się to niemożliwe.Nie korzystałem z Google Guice, ale poświęciłem dużo czasu na migrację starych, starszych aplikacji N-warstwy w .Net do architektur IoC, takich jak Onion Layer, które zależą od wstrzykiwania zależności, aby oddzielić rzeczy.
Dlaczego wstrzykiwanie zależności?
Zadanie Dependency Injection nie jest tak naprawdę testowalne, lecz polega na przyjmowaniu ściśle sprzężonych aplikacji i poluzowaniu sprzężenia tak bardzo, jak to możliwe. (Co jest pożądane przez produkt, dzięki czemu kod jest znacznie łatwiejszy do dostosowania do prawidłowego testowania jednostkowego)
Dlaczego w ogóle mam się martwić sprzężeniem?
Sprzężenie lub ścisłe zależności mogą być bardzo niebezpieczne. (Zwłaszcza w językach kompilowanych) W takich przypadkach możesz mieć bibliotekę, bibliotekę DLL itp., Która jest bardzo rzadko używana i ma problem, który powoduje, że cała aplikacja jest offline. (Cała twoja aplikacja umiera, ponieważ problem nieważnego elementu ... jest zły ... NAPRAWDĘ zły) Teraz, kiedy odsprzęgasz rzeczy, możesz faktycznie skonfigurować aplikację, aby mogła działać, nawet jeśli brakuje tej biblioteki DLL lub biblioteki! Upewnij się, że jeden element, który potrzebuje tej biblioteki lub biblioteki DLL, nie będzie działał, ale reszta aplikacji będzie się starać, jak to możliwe.
Dlaczego potrzebuję wtrysku zależności do prawidłowego testowania
Naprawdę chcesz po prostu luźno połączonego kodu, Dependency Injection po prostu to umożliwia. Możesz luźno powiązać rzeczy bez IoC, ale zazwyczaj jest to więcej pracy i mniej adaptowalne (jestem pewien, że ktoś ma wyjątek)
W przypadku, który podałeś, myślę, że o wiele łatwiej byłoby po prostu skonfigurować wstrzykiwanie zależności, aby móc wyśmiewać kod, który nie jest zainteresowany liczeniem w ramach tego testu. Po prostu powiedz metodę „Hej, wiem, że kazałem ci wywołać repozytorium, ale zamiast tego oto dane, które powinny teraz„ mrugnąć ”teraz, ponieważ te dane nigdy się nie zmieniają, wiesz, że testujesz tylko część, która wykorzystuje te dane, a nie faktyczne pobieranie danych.
Zasadniczo podczas testowania potrzebna jest integracja (funkcjonalność) Testy, które testują funkcjonalność od początku do końca, oraz pełne testy jednostkowe, które testują każdy fragment kodu (zazwyczaj na poziomie metody lub funkcji) niezależnie.
Chodzi o to, aby upewnić się, że cała funkcjonalność działa, a jeśli nie, chcesz znać dokładny fragment kodu, który nie działa.
To MOŻE być wykonane bez Dependency Injection, ale zwykle jako projektu rośnie staje się coraz bardziej uciążliwe, aby to zrobić bez Dependency Injection w miejscu. (ZAWSZE zakładaj, że Twój projekt się rozrośnie! Lepiej jest niepotrzebnie ćwiczyć przydatne umiejętności, niż znaleźć projekt, który szybko się rozwija i wymaga poważnego przebudowania i przeprojektowania po tym, jak wszystko już się zaczyna.)
źródło
Jak wspomniałem w innej odpowiedzi , problem polega na tym, że chcesz, aby klasa
A
zależała od jakiejś klasyB
bez twardego kodowania, która klasaB
jest używana w kodzie źródłowym A. Jest to niemożliwe w Javie i C #, ponieważ jest to jedyny sposób na zaimportowanie klasy jest odwoływanie się do niego unikalną na skalę globalną nazwą.Korzystając z interfejsu, możesz obejść sztywno zakodowaną zależność klas, ale nadal musisz zdobyć instancję interfejsu i nie możesz wywoływać konstruktorów lub wracasz do kwadratu 1. Więc teraz kod które w przeciwnym razie mogłyby stworzyć jego zależności, spychają odpowiedzialność na kogoś innego. I jego zależności robią to samo. Tak więc teraz za każdym razem, gdy potrzebujesz instancji klasy, budujesz ręcznie całe drzewo zależności, natomiast w przypadku, gdy klasa A zależy bezpośrednio od B, możesz po prostu wywołać
new A()
i wywołać to wywołanie konstruktoranew B()
i tak dalej.Struktura wstrzykiwania zależności próbuje obejść ten problem, umożliwiając określenie odwzorowań między klasami i zbudowanie drzewa zależności. Problem polega na tym, że kiedy spieprzysz mapowania, dowiesz się w czasie wykonywania, a nie w czasie kompilacji, tak jak w językach, które obsługują moduły mapowania jako koncepcja pierwszej klasy.
źródło
Myślę, że jest to duże nieporozumienie.
Guice jest wtrysk zależność ramy . To sprawia, że DI jest automatyczne . W cytowanym przez Ciebie fragmencie chodzi o to, że Guice może usunąć potrzebę ręcznego tworzenia „testowalnego konstruktora”, który przedstawiłeś w swoim przykładzie. Nie ma to absolutnie nic wspólnego z samym zastrzykiem zależności.
Ten konstruktor:
już używa wstrzykiwania zależności. Po prostu powiedziałeś, że korzystanie z DI jest łatwe.
Problem, który rozwiązuje Guice, polega na tym, że aby użyć tego konstruktora, musisz mieć gdzieś kod konstruktora z grafem obiektowym , ręcznie przekazując już utworzone instancje jako argumenty tego konstruktora. Guice pozwala mieć jedno miejsce, w którym możesz skonfigurować, które prawdziwe klasy implementacji odpowiadają tym
CreditCardProcessor
iTransactionLog
interfejsom. Po tej konfiguracji za każdym razem, gdy tworzyszBillingService
za pomocą Guice, klasy te będą automatycznie przekazywane do konstruktora.Tak działa struktura wstrzykiwania zależności . Ale sam konstruktor, który przedstawiłeś, jest już implementacją zasady wstrzykiwania depencencji . Kontenery IoC i frameworki DI są środkami do automatyzacji odpowiednich zasad, ale nic nie stoi na przeszkodzie, abyś robił wszystko ręcznie, o to właśnie chodziło.
źródło