Od czasu uczenia się (i uwielbiania) automatycznych testów, stosuję wzorzec wstrzykiwania zależności w prawie każdym projekcie. Czy zawsze należy stosować ten wzorzec podczas pracy z testami automatycznymi? Czy istnieją sytuacje, w których należy unikać wstrzykiwania zależności?
design-patterns
dependency-injection
Tom Squires
źródło
źródło
Odpowiedzi:
Zasadniczo wstrzykiwanie zależności przyjmuje pewne (zwykle, ale nie zawsze poprawne) założenia dotyczące natury twoich obiektów. Jeśli są one błędne, DI może nie być najlepszym rozwiązaniem:
Po pierwsze, najbardziej zasadniczo, DI zakłada, że ścisłe sprzężenie implementacji obiektów ZAWSZE jest złe . Taka jest istota zasady odwrócenia zależności: „nigdy nie należy uzależniać się od konkrecji; tylko od abstrakcji”.
To zamyka zależny obiekt do zmiany na podstawie zmiany konkretnej implementacji; klasa zależna od ConsoleWriter będzie musiała się zmienić, jeśli dane wyjściowe będą musiały przejść do pliku, ale jeśli klasa była zależna tylko od IWriter ujawniającego metodę Write (), możemy zastąpić ConsoleWriter aktualnie używaną FileWriter i naszym klasa zależna nie znałaby różnicy (Zasada podstawienia Liskhova).
Jednak projekt NIGDY nie może być zamknięty na wszystkie rodzaje zmian; jeśli zmienia się konstrukcja samego interfejsu IWriter, aby dodać parametr do Write (), należy teraz zmienić dodatkowy obiekt kodowy (interfejs IWriter), nad obiektem / metodą implementacji i jego wykorzystaniem. Jeśli zmiany w rzeczywistym interfejsie są bardziej prawdopodobne niż zmiany w implementacji wspomnianego interfejsu, luźne sprzężenie (i zależności luźno sprzężone DI) mogą powodować więcej problemów niż rozwiązuje.
Po drugie, DI i zakłada, że klasa zależna NIGDY nie jest dobrym miejscem do stworzenia zależności . Odnosi się to do zasady jednolitej odpowiedzialności; jeśli masz kod, który tworzy zależność i również z niej korzysta, istnieją dwa powody, dla których klasa zależna może być zmuszona do zmiany (zmiana użycia LUB implementacji), naruszając SRP.
Ponownie jednak dodanie warstw pośrednich dla DI może być rozwiązaniem problemu, który nie istnieje; jeśli logiczne jest zawarcie logiki w zależności, ale ta logika jest jedyną taką implementacją zależności, wówczas bardziej bolesne jest kodowanie luźno sprzężonego rozwiązania zależności (wstrzyknięcie, lokalizacja usługi, fabryka) niż byłoby użyć
new
i zapomnieć o tym.Wreszcie, DI ze swej natury centralizuje wiedzę o wszystkich zależnościach i ich implementacjach . Zwiększa to liczbę referencji, które musi posiadać zespół wykonujący wstrzyknięcie, aw większości przypadków NIE zmniejsza liczby referencji wymaganych przez rzeczywiste zestawy klas zależnych.
COŚ, GDZIEŚ, musi mieć wiedzę na temat zależności, interfejsu zależności i implementacji zależności, aby „połączyć kropki” i zaspokoić tę zależność. DI ma tendencję do umieszczania całej tej wiedzy na bardzo wysokim poziomie, albo w kontenerze IoC, albo w kodzie tworzącym „główne” obiekty, takie jak główna postać lub kontroler, które muszą uwodnić (lub zapewnić metody fabryczne) zależności. Może to umieścić wiele koniecznie ściśle powiązanych kodów i wiele odniesień do asemblera na wysokich poziomach twojej aplikacji, która potrzebuje tylko tej wiedzy, aby „ukryć” ją przed rzeczywistymi klasami zależnymi (która z bardzo podstawowej perspektywy jest najlepsze miejsce na posiadanie tej wiedzy; gdzie jest używana).
Zwykle również nie usuwa wspomnianych odniesień z niższej części kodu; osoba zależna musi nadal odwoływać się do biblioteki zawierającej interfejs ze względu na jej zależność, która znajduje się w jednym z trzech miejsc:
Wszystko to ponownie rozwiązuje problem w miejscach, w których może ich nie być.
źródło
Poza strukturami wstrzykiwania zależności, wstrzykiwanie zależności (poprzez wstrzykiwanie konstruktora lub wstrzykiwanie settera) jest prawie grą o sumie zerowej: zmniejszasz sprzężenie między obiektem A i jego zależnością B, ale teraz każdy obiekt, który potrzebuje wystąpienia A, musi teraz również skonstruuj obiekt B.
Nieznacznie zmniejszyłeś sprzężenie między A i B, ale zmniejszyłeś hermetyzację A i zwiększyłeś sprzężenie między A i dowolną klasą, która musi zbudować instancję A, poprzez sprzężenie ich również z zależnościami A.
Zatem wstrzykiwanie zależności (bez frameworka) jest równie szkodliwe, jak pomocne.
Dodatkowy koszt jest często łatwo uzasadniony: jeśli kod klienta wie więcej o tym, jak zbudować zależność niż sam obiekt, to wstrzyknięcie zależności naprawdę zmniejsza sprzężenie; na przykład skaner nie wie dużo o tym, jak uzyskać lub skonstruować strumień wejściowy do analizy danych wejściowych, ani z jakiego źródła kod klienta chce analizować dane wejściowe, więc wstrzyknięcie konstruktora strumienia wejściowego jest oczywistym rozwiązaniem.
Testowanie jest kolejnym uzasadnieniem, aby móc użyć próbnych zależności. Powinno to oznaczać dodanie dodatkowego konstruktora używanego tylko do testowania, który pozwala wstrzykiwać zależności: jeśli zamiast tego zmienisz konstruktory tak, aby zawsze wymagały wstrzykiwania zależności, nagle musisz wiedzieć o zależnościach zależności w celu zbudowania swojej bezpośrednie zależności i nie można wykonać żadnej pracy.
Może to być pomocne, ale zdecydowanie powinieneś zadać sobie pytanie o każdą zależność, czy korzyść z testowania jest warta kosztu i czy naprawdę będę chciał kpić z tej zależności podczas testowania?
Po dodaniu struktury wstrzykiwania zależności, a konstrukcja zależności jest delegowana nie do kodu klienta, ale do struktury, analiza kosztów / korzyści zmienia się znacznie.
W ramach wstrzykiwania zależności kompromisy są nieco inne; to, co tracisz przez wstrzyknięcie zależności, to możliwość łatwego dowiedzenia się, na której implementacji się opierasz, i przeniesienie odpowiedzialności za podjęcie decyzji o zależności, na której polegasz, na proces automatycznego rozwiązywania problemów (np. jeśli potrzebujemy Foo @ Inject'ed , musi być coś, co @Provides Foo i którego wstrzyknięte zależności są dostępne), lub jakiś plik konfiguracyjny wysokiego poziomu, który określa, jakiego dostawcę należy użyć dla każdego zasobu, lub jakiejś hybrydy tych dwóch (na przykład może istnieć być zautomatyzowanym procesem rozwiązywania zależności, które w razie potrzeby można zastąpić za pomocą pliku konfiguracyjnego).
Podobnie jak w przypadku iniekcji konstruktora, myślę, że korzyść z tego jest znowu bardzo podobna do kosztu: nie musisz wiedzieć, kto dostarcza dane, na których polegasz, a jeśli istnieje wiele potencjalnych dostawców, nie musisz znać preferowanej kolejności sprawdzania dostawców, upewnij się, że każda lokalizacja, która potrzebuje kontroli danych dla wszystkich potencjalnych dostawców itp., ponieważ wszystko jest obsługiwane na wysokim poziomie przez wstrzyknięcie zależności Platforma.
Chociaż osobiście nie mam dużego doświadczenia z frameworkami DI, mam wrażenie, że zapewniają one więcej korzyści niż kosztów, gdy kłopot ze znalezieniem właściwego dostawcy potrzebnych danych lub usług jest wyższy niż ból głowy, gdy coś zawiedzie, nie wiedząc od razu, jaki kod dostarczył złe dane, co spowodowało późniejszą awarię kodu.
W niektórych przypadkach inne wzorce, które zaciemniają zależności (np. Lokalizatory usług), zostały już przyjęte (a być może także udowodniły swoją wartość), gdy na scenie pojawiły się frameworki DI, a frameworki DI zostały przyjęte, ponieważ oferowały pewną przewagę konkurencyjną, na przykład wymagały mniej kodu podstawowego lub potencjalnie robiąc mniej, aby ukryć dostawcę zależności, gdy konieczne będzie ustalenie, który dostawca faktycznie jest w użyciu.
źródło
jeśli tworzysz jednostki bazy danych, powinieneś raczej mieć klasę fabryczną, którą zamiast tego wstrzykniesz do kontrolera,
jeśli chcesz stworzyć prymitywne obiekty, takie jak ints lub longs. Powinieneś także utworzyć „ręcznie” większość standardowych obiektów bibliotecznych, takich jak daty, przewodniki itp.
jeśli chcesz wstrzykiwać ciągi konfiguracji, prawdopodobnie lepiej jest wstrzyknąć niektóre obiekty konfiguracji (ogólnie zaleca się zawijanie prostych typów w znaczące obiekty: int temperatureInCelsiusDegrees -> temperatura CelciusDeegree)
I nie używaj Lokalizatora usług jako alternatywy wstrzykiwania zależności, jest to anty-wzorzec, więcej informacji: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx
źródło
Gdy nie możesz nic zyskać, czyniąc projekt możliwym do utrzymania i przetestowania.
Poważnie, uwielbiam ogólnie IoC i DI i powiedziałbym, że 98% czasu będę używać tego wzorca bezbłędnie. Jest to szczególnie ważne w środowisku wielu użytkowników, w którym kod może być wielokrotnie wykorzystywany przez różnych członków zespołu i różne projekty, ponieważ oddziela logikę od implementacji. Rejestrowanie jest tego najlepszym przykładem. Interfejs ILog wprowadzony do klasy jest tysiąc razy łatwiejszy w utrzymaniu niż zwykłe podłączenie twojego środowiska rejestrowania-du-Jour, ponieważ nie masz gwarancji, że inny projekt użyje tego samego środowiska rejestrowania (jeśli używa jeden w ogóle!).
Są jednak chwile, kiedy nie jest to odpowiedni wzór. Na przykład funkcjonalne punkty wejścia, które są implementowane w kontekście statycznym przez nieprzekraczalny inicjator (WebMethods, patrzę na ciebie, ale twoja metoda Main () w klasie Program jest innym przykładem) po prostu nie może mieć wstrzykniętych zależności przy inicjalizacji czas. Chciałbym również powiedzieć, że prototyp lub dowolny wyrzucony fragment kodu śledczego jest również złym kandydatem; korzyści z DI to w przybliżeniu korzyści średnio- i długoterminowe (testowalność i łatwość konserwacji), jeśli jesteś pewien, że wyrzucisz większość kodu w ciągu tygodnia lub mniej więcej powiedziałbym, że nic nie zyskasz izolując zależności, po prostu spędzaj czas, który normalnie spędzasz na testowaniu i izolowaniu zależności, aby uruchomić kod.
Podsumowując, rozsądne jest przyjęcie pragmatycznego podejścia do dowolnej metodologii lub schematu, ponieważ nic nie ma zastosowania w 100% przypadków.
Jedną z rzeczy, na które należy zwrócić uwagę, jest twój komentarz na temat testów automatycznych: moja definicja tego to zautomatyzowane testy funkcjonalne , na przykład skrypty selenu, jeśli jesteś w kontekście sieci. Są to na ogół testy całkowicie czarnej skrzynki, bez potrzeby wiedzieć o wewnętrznych działaniach kodu. Jeśli miałeś na myśli testy jednostkowe lub integracyjne, powiedziałbym, że wzorzec DI prawie zawsze ma zastosowanie do każdego projektu, który w dużej mierze opiera się na tego rodzaju testach białych skrzynek, ponieważ na przykład pozwala testować takie rzeczy, jak metody dotykające DB bez potrzeby obecności DB.
źródło
Podczas gdy inne odpowiedzi koncentrują się na aspektach technicznych, chciałbym dodać praktyczny wymiar.
Przez lata doszedłem do wniosku, że istnieje kilka praktycznych wymagań, które należy spełnić, aby wprowadzenie Dependency Injection miało odnieść sukces.
Powód powinien być uzasadniony.
Wydaje się to oczywiste, ale jeśli twój kod pobiera tylko rzeczy z bazy danych i zwraca je bez logiki, to dodanie kontenera DI sprawia, że rzeczy są bardziej złożone i nie przynoszą rzeczywistych korzyści. Ważniejsze byłyby tutaj testy integracyjne.
Zespół musi być przeszkolony i znajdować się na pokładzie.
Chyba że większość członków zespołu jest na pokładzie i nie rozumie, że dodanie odwrócenia kontenera sterującego staje się jeszcze jednym sposobem na zrobienie czegoś i jeszcze bardziej komplikuje bazę kodu.
Jeśli DI zostanie wprowadzony przez nowego członka zespołu, ponieważ rozumieją go i podoba im się, a po prostu chcą pokazać, że są dobrzy, ORAZ zespół nie jest aktywnie zaangażowany, istnieje realne ryzyko, że faktycznie obniży jakość kod.
Musisz przetestować
Podczas gdy odsprzęganie jest ogólnie dobrą rzeczą, DI może przenosić rozdzielczość zależności z czasu kompilacji do czasu wykonywania. Jest to dość niebezpieczne, jeśli nie przetestujesz dobrze. Błędy rozwiązywania w czasie wykonywania mogą być kosztowne w śledzeniu i rozwiązywaniu.
(Z testu wynika, że to robisz, ale wiele zespołów nie testuje w zakresie wymaganym przez DI.)
źródło
To nie jest pełna odpowiedź, ale tylko kolejny punkt.
Gdy masz aplikację, która uruchamia się raz, działa przez długi czas (jak aplikacja internetowa) DI może być dobry.
Jeśli masz aplikację, która uruchamia się wiele razy i działa krócej (np. Aplikacja mobilna), prawdopodobnie nie chcesz kontenera.
źródło
Spróbuj użyć podstawowych zasad OOP: użyj dziedziczenia, aby wyodrębnić wspólną funkcjonalność, obudować (ukryć) rzeczy, które powinny być chronione przed światem zewnętrznym za pomocą prywatnych / wewnętrznych / chronionych członków / typów. Użyj dowolnego zaawansowanego środowiska testowego, aby wstrzyknąć kod tylko do testów, na przykład https://www.typemock.com/ lub https://www.telerik.com/products/mocking.aspx .
Następnie spróbuj ponownie napisać go za pomocą DI i porównać kod, co zwykle zobaczysz za pomocą DI:
Powiedziałbym, że z tego, co widziałem, prawie zawsze jakość kodu spada z DI.
Jeśli jednak użyjesz tylko „publicznego” modyfikatora dostępu w deklaracji klasy i / lub publicznych / prywatnych modyfikatorów dla członków i / lub nie masz wyboru, aby kupić drogie narzędzia testowe i jednocześnie potrzebujesz testów jednostkowych, które mogą „ zostać zastąpiony testami integracyjnymi i / lub masz już interfejsy dla klas, które zamierzasz wprowadzić, DI to dobry wybór!
ps prawdopodobnie dostanę wiele minusów za ten post, wierzę, ponieważ większość współczesnych programistów po prostu nie rozumie, jak i dlaczego używać wewnętrznego słowa kluczowego i jak zmniejszyć sprzężenie twoich komponentów i wreszcie dlaczego je zmniejszyć) w końcu, po prostu spróbuj kodować i porównywać
źródło
Alternatywą dla Dependency Injection używa lokalizatora usług . Lokalizator usług jest łatwiejszy do zrozumienia, debugowania i upraszcza konstruowanie obiektu, zwłaszcza jeśli nie używasz środowiska DI. Lokalizatory usług są dobrym wzorcem do zarządzania zewnętrznymi zależnościami statycznymi , na przykład bazą danych, którą w innym przypadku musiałbyś przekazać do każdego obiektu w warstwie dostępu do danych.
Podczas refaktoryzacji starszego kodu często łatwiej jest refaktoryzować do lokalizatora usług niż do wstrzykiwania zależności. Wystarczy zastąpić instancje przeglądami usług, a następnie sfałszować usługę w teście jednostkowym.
Istnieją jednak pewne wady Lokalizatora usług. Znajomość zależności klasy jest trudniejsza, ponieważ zależności są ukryte w implementacji klasy, a nie w konstruktorach lub ustawieniach. A tworzenie dwóch obiektów, które opierają się na różnych implementacjach tej samej usługi, jest trudne lub niemożliwe.
TLDR : Jeśli klasa ma zależności statyczne lub refaktoryzuje starszy kod, Lokalizator usług jest prawdopodobnie lepszy niż DI.
źródło