Kwestionując jeden z argumentów dotyczących struktur wstrzykiwania zależności: Dlaczego tworzenie wykresu obiektowego jest trudne?

13

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?

oberlies
źródło
1
Myślę, że kolejną zaletą zastrzyku zależności, która jest często ignorowana, jest to, że zmusza cię do poznania, od czego zależą obiekty . Nic nie pojawia się magicznie w obiektach, albo wstrzykujesz z wyraźnie zaznaczonymi atrybutami, albo bezpośrednio przez konstruktora. Nie wspominając już o tym, jak sprawia, że ​​twój kod jest bardziej testowalny.
Benjamin Gruenbaum,
Zauważ, że nie szukam innych zalet wstrzykiwania zależności - interesuje mnie tylko zrozumienie argumentu, że „tworzenie instancji obiektu jest trudne bez wstrzyknięcia zależności”
oberlies
Ta odpowiedź wydaje się stanowić bardzo dobry przykład, dlaczego chcesz mieć jakiś pojemnik na swoje wykresy obiektowe (krótko mówiąc, nie myślisz wystarczająco duży, używając tylko 3 obiektów).
Izkata
@Izkata Nie, to nie jest kwestia rozmiaru. Przy opisanym podejściu kod z tego postu byłby po prostu new ShippingService().
oberlies
@oberlies Still is; musiałbyś wtedy zagłębić się w 9 definicji klas, aby dowiedzieć się, które klasy są używane dla tej Usługi Wysyłki, a nie jednej lokalizacji
Izkata

Odpowiedzi:

12

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 PaypalCreditCardProcessori DatabaseTransactionLog, 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ć.

Obrabować
źródło
Kod przejmie odpowiedzialność za zbudowanie tego wszystkiego - klasa bierze odpowiedzialność tylko za to, jakie są jej implementacje współpracujące. Nie musi wiedzieć, czego ci koledzy potrzebują z kolei. To nie jest tak dużo.
oberlies
1
Ale jeśli konstruktor PayPalProcessor miał 2 argumenty, to skąd one się wzięły?
Rob
Tak nie jest. Podobnie jak BillingService, PayPalProcessor ma konstruktor zero-args, który tworzy współpracowników, których sam potrzebuje.
oberlies
wyodrębnianie zależności podrzędnych do interfejsów - przykładowy kod również to robi. Pole procesora jest typu CreditCardProcessor, a nie typu implementacji.
oberlies
2
@oberlies: Twój przykład kodu znajduje się pomiędzy dwoma stylami, stylem argumentu konstruktora i stylem konstrukcyjnym zero-args. Błędem jest to, że konstrukcja zerowego argumentu nie zawsze jest możliwa. Piękno DI lub Factory polega na tym, że w jakiś sposób pozwalają one na konstruowanie obiektów, które wyglądają jak konstruktor zero-args.
rwong
4
  1. Kiedy pływasz na płytkim końcu basenu, wszystko jest „łatwe i wygodne”. Gdy miniesz kilkanaście obiektów, nie jest to już wygodne.
  2. W twoim przykładzie proces fakturowania związałeś z PayPalem na zawsze. Załóżmy, że chcesz użyć innego procesora karty kredytowej? Załóżmy, że chcesz utworzyć specjalny procesor karty kredytowej, który jest ograniczony w sieci? A może musisz przetestować obsługę numerów kart kredytowych? Utworzyłeś nieprzenośny kod: „napisz raz, użyj tylko raz, ponieważ zależy to od konkretnego wykresu obiektowego, dla którego został zaprojektowany”.

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.

BobDalgleish
źródło
Kiedy podążę za twoim argumentem, rozumowanie DI powinno być takie, że „ręczne tworzenie wykresu obiektowego jest łatwe, ale prowadzi do kodu, który trudno utrzymać”. Twierdzenie było jednak takie, że „ręczne tworzenie wykresu obiektowego jest trudne” i szukam tylko argumentów na poparcie tego twierdzenia. Twój post nie odpowiada na moje pytanie.
oberlies
1
Myślę, że jeśli zmniejszy pytanie do „tworzenia się wykres obiektu ...”, to jest proste. Chodzi mi o to, że DI rozwiązuje problem, który nigdy nie dotyczy tylko jednego wykresu obiektowego; masz do czynienia z ich rodziną.
BobDalgleish
W rzeczywistości tworzenie jednego wykresu obiektowego może być już trudne, jeśli współpracownicy są współużytkowani .
oberlies
4

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ę.

oberlies
źródło
1
Ale drzewo zależności musi być drzewem w sensie teorii grafów. W przeciwnym razie masz cykl, który jest po prostu niezadowalający.
Xion
1
@Xion Wykres zależności musi być ukierunkowanym wykresem acyklicznym. Nie ma wymogu, aby między dwoma węzłami istniała tylko jedna ścieżka, jak w drzewach.
oberlies
@Xion: Niekoniecznie. Zastanów się, UnitOfWorkktóry ma jedno DbContexti wiele repozytoriów. Wszystkie te repozytoria powinny używać tego samego DbContextobiektu. Dzięki zaproponowanemu przez OP „autodetekcji” staje się to niemożliwe.
Flater
1

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.)

RualStorge
źródło
1
Pisanie testów przy użyciu dużej części, ale nie wszystkich wykresów zależności, jest w rzeczywistości dobrym argumentem dla DI.
oberlies
1
Warto również zauważyć, że w DI można łatwo programowo zamieniać całe fragmenty kodu. Widziałem przypadki, w których system wyczyściłby i ponownie wprowadził interfejs o zupełnie innej klasie, aby reagować na awarie i problemy z wydajnością zdalnych usług. Być może istniał lepszy sposób, aby sobie z tym poradzić, ale zadziałało zaskakująco dobrze.
RualStorge
0

Jak wspomniałem w innej odpowiedzi , problem polega na tym, że chcesz, aby klasa Azależała od jakiejś klasyB bez twardego kodowania, która klasa Bjest 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 konstruktora new 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.

Doval
źródło
0

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:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

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 CreditCardProcessori TransactionLoginterfejsom. Po tej konfiguracji za każdym razem, gdy tworzysz BillingServiceza 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.

hijarian
źródło