Próbuję zrozumieć zastrzyki zależności (DI) i po raz kolejny zawiodłem. To po prostu wydaje się głupie. Mój kod nigdy nie jest bałaganem; Prawie nie piszę funkcji wirtualnych i interfejsów (chociaż robię to raz na niebieskim księżycu), a cała moja konfiguracja jest magicznie serializowana do klasy za pomocą json.net (czasem za pomocą serializatora XML).
Nie do końca rozumiem, jaki problem rozwiązuje. Wygląda to na sposób powiedzenia: „cześć. Po uruchomieniu tej funkcji zwróć obiekt tego typu i wykorzystujący te parametry / dane”.
Ale ... dlaczego miałbym tego używać? Uwaga: Nigdy też nie musiałem używać object
, ale rozumiem, do czego to służy.
Jakie są rzeczywiste sytuacje w budowaniu strony internetowej lub aplikacji komputerowej, w których można użyć DI? Mogę łatwo wymyślić przypadki, dla których ktoś może chcieć używać interfejsów / funkcji wirtualnych w grze, ale jest to niezwykle rzadkie (na tyle rzadkie, że nie pamiętam ani jednej instancji) użycie tego w kodzie innym niż gra.
źródło
Odpowiedzi:
Po pierwsze, chcę wyjaśnić założenie, że przyjmę tę odpowiedź. Nie zawsze jest to prawda, ale dość często:
(W rzeczywistości istnieją również interfejsy, które są rzeczownikami, ale chcę tutaj uogólnić.)
Tak więc np. Interfejs może być czymś takim jak
IDisposable
,IEnumerable
lubIPrintable
. Klasa jest faktyczną implementacją jednego lub więcej z tych interfejsów:List
lubMap
obie mogą być implementacjamiIEnumerable
.Aby uzyskać punkt: często twoje zajęcia zależą od siebie. Np. Możesz mieć
Database
klasę, która uzyskuje dostęp do twojej bazy danych (hah, niespodzianka! ;-)), ale chcesz też, aby ta klasa rejestrowała dostęp do bazy danych. Załóżmy, że masz inną klasęLogger
, a następnieDatabase
zależność od niejLogger
.Na razie w porządku.
Możesz modelować tę zależność w swojej
Database
klasie za pomocą następującego wiersza:i wszystko jest w porządku. Do dnia, w którym zdasz sobie sprawę, że potrzebujesz kilku rejestratorów, jest w porządku: Czasami chcesz zalogować się do konsoli, czasem do systemu plików, czasem używając TCP / IP i zdalnego serwera rejestrowania i tak dalej ...
I oczywiście NIE chcesz zmieniać całego kodu (tymczasem masz jego gazilliony) i zastępować wszystkie wiersze
przez:
Po pierwsze, to nie jest zabawa. Po drugie, jest to podatne na błędy. Po trzecie, jest to głupia, powtarzalna praca dla wyszkolonej małpy. Więc co robisz?
Oczywiście całkiem dobrym pomysłem jest wprowadzenie interfejsu
ICanLog
(lub podobnego), który jest implementowany przez wszystkie różne programy rejestrujące. Więc krok 1 w twoim kodzie to:Teraz wnioskowanie o typach nie zmienia już typu, zawsze masz jeden interfejs do opracowania. Następnym krokiem jest to, że nie chcesz mieć
new Logger()
w kółko. Dlatego stawiasz niezawodność, aby tworzyć nowe wystąpienia w jednej, centralnej klasie fabryki, i otrzymujesz kod, taki jak:Sama fabryka decyduje, jaki rodzaj rejestratora utworzyć. Twój kod już się nie przejmuje, a jeśli chcesz zmienić typ używanego rejestratora, zmienisz go raz : wewnątrz fabryki.
Teraz oczywiście możesz uogólnić tę fabrykę i sprawić, by działała dla dowolnego typu:
Gdzieś ten TypeFactory potrzebuje danych konfiguracyjnych, które rzeczywista klasa utworzy, gdy zostanie zażądany określony typ interfejsu, więc potrzebujesz mapowania. Oczywiście możesz wykonać to mapowanie w kodzie, ale zmiana typu oznacza rekompilację. Ale możesz również umieścić to mapowanie w pliku XML, np. Pozwala to zmienić faktycznie używaną klasę nawet po czasie kompilacji (!), Co oznacza dynamicznie, bez ponownej kompilacji!
Aby dać ci użyteczny przykład: Pomyśl o oprogramowaniu, które nie loguje się normalnie, ale kiedy klient dzwoni i prosi o pomoc, ponieważ ma problem, wszystko, co mu wysyłasz, to zaktualizowany plik konfiguracyjny XML, a teraz ma rejestrowanie włączone, a obsługa klienta może użyć plików dziennika, aby pomóc klientowi.
A teraz, kiedy trochę zastąpisz nazwy, kończysz się prostą implementacją Lokalizatora usług , który jest jednym z dwóch wzorców Inwersji Kontroli (ponieważ odwracasz kontrolę nad tym, kto decyduje, którą konkretną klasę utworzyć).
Podsumowując, zmniejsza to zależności w kodzie, ale teraz cały kod jest zależny od centralnego lokalizatora pojedynczej usługi.
Wstrzykiwanie zależności jest teraz kolejnym krokiem w tej linii: pozbądź się tej pojedynczej zależności od lokalizatora usług: zamiast różnych klas pytających lokalizator usług o implementację dla określonego interfejsu, ty - po raz kolejny - przywracasz kontrolę nad tym, kto tworzy co .
Dzięki wstrzykiwaniu zależności
Database
klasa ma teraz konstruktor, który wymaga parametru typuICanLog
:Teraz twoja baza danych zawsze ma logger do użycia, ale nie wie już, skąd ten logger pochodzi.
I tu właśnie pojawia się środowisko DI: Ponownie konfigurujesz swoje odwzorowania, a następnie pytasz środowisko DI o utworzenie dla Ciebie aplikacji. Ponieważ
Application
klasa wymagaICanPersistData
implementacji, instancjaDatabase
jest wstrzykiwana - ale w tym celu musi najpierw utworzyć instancję tego rodzaju programu rejestrującego, dla którego jest skonfigurowanaICanLog
. I tak dalej ...Krótko mówiąc: wstrzykiwanie zależności jest jednym z dwóch sposobów usuwania zależności w kodzie. Jest to bardzo przydatne do zmian konfiguracji po czasie kompilacji i jest świetną rzeczą do testowania jednostkowego (ponieważ bardzo łatwo wstrzykuje kody pośredniczące i / lub próbne).
W praktyce są rzeczy, których nie można obejść bez lokalizatora usług (np. Jeśli nie wiesz z góry, ile instancji potrzebujesz konkretnego interfejsu: Framework DI zawsze wstrzykuje tylko jedną instancję na parametr, ale możesz wywołać lokalizator usług wewnątrz pętli, oczywiście), dlatego najczęściej każda struktura DI zapewnia również lokalizator usług.
Ale w zasadzie to tyle.
PS: To, co tu opisałem, to technika nazywana wstrzykiwaniem konstruktora , jest też wstrzykiwanie właściwości, gdzie nie są parametry konstruktora, ale właściwości są używane do definiowania i rozwiązywania zależności. Pomyśl o wstrzykiwaniu właściwości jako opcjonalnej zależności, a o wstrzykiwaniu konstruktora o obowiązkowych zależności. Ale dyskusja na ten temat wykracza poza zakres tego pytania.
źródło
Myślę, że wiele razy ludzie się mylić o różnicę między wstrzykiwania zależności i iniekcji zależność ram (lub pojemnika jak to często nazywa).
Zastrzyk zależności jest bardzo prostą koncepcją. Zamiast tego kodu:
piszesz taki kod:
I to wszystko. Poważnie. Daje to wiele korzyści. Dwie ważne z nich to zdolność do kontrolowania funkcjonalności z centralnego miejsca (
Main()
funkcji) zamiast rozprowadzania jej w całym programie oraz zdolność do łatwiejszego testowania każdej klasy w izolacji (ponieważ zamiast tego można przekazywać symulatory lub inne fałszywe obiekty do konstruktora o rzeczywistej wartości).Wadą jest oczywiście to, że masz teraz jedną mega-funkcję, która wie o wszystkich klasach używanych przez twój program. W tym mogą pomóc frameworki DI. Ale jeśli masz problemy ze zrozumieniem, dlaczego to podejście jest tak cenne, zalecam najpierw ręczne wstrzyknięcie zależności, abyś mógł lepiej docenić, co mogą dla ciebie zrobić różne frameworki.
źródło
Jak stwierdzono w innych odpowiedziach, wstrzykiwanie zależności jest sposobem na tworzenie zależności poza klasą, która z nich korzysta. Wstrzykujesz je z zewnątrz i przejmujesz kontrolę nad ich stworzeniem z wnętrza klasy. Dlatego też wstrzykiwanie zależności jest realizacją zasady Inversion of control (IoC).
IoC jest zasadą, w której DI jest wzorem. Z mojego doświadczenia wynika, że możesz potrzebować więcej niż jednego rejestratora, ale tak naprawdę to dlatego, że naprawdę go potrzebujesz, za każdym razem, gdy coś testujesz. Przykład:
Moja funkcja:
Możesz to przetestować w następujący sposób:
Gdzieś w
OfferWeasel
buduje ci ofertę Obiekt taki jak ten:Problem polega na tym, że ten test najprawdopodobniej zawsze się nie powiedzie, ponieważ ustawiana data będzie się różnić od daty, o której mowa, nawet jeśli po prostu wstawisz
DateTime.Now
kod testowy, może on być wyłączony o kilka milisekund, a zatem zawsze zawodzi. Lepszym rozwiązaniem byłoby teraz utworzenie do tego interfejsu, który pozwala kontrolować, która godzina zostanie ustawiona:Interfejs jest abstrakcją. Jedna jest PRAWDZIWĄ rzeczą, a druga pozwala udawać, że jest potrzebna. Test można następnie zmienić w następujący sposób:
W ten sposób zastosowałeś zasadę „inwersji kontroli”, wstrzykując zależność (uzyskanie bieżącego czasu). Głównym powodem tego jest łatwiejsze izolowane testowanie jednostek, istnieją inne sposoby na to. Na przykład interfejs i klasa tutaj nie są potrzebne, ponieważ w języku C # funkcje mogą być przekazywane jako zmienne, więc zamiast interfejsu można użyć a,
Func<DateTime>
aby osiągnąć to samo. Lub, jeśli zastosujesz podejście dynamiczne, po prostu przekazujesz dowolny obiekt, który ma równoważną metodę ( pisanie kaczką ), i wcale nie potrzebujesz interfejsu.Prawie nigdy nie będziesz potrzebować więcej niż jednego rejestratora. Niemniej jednak wstrzykiwanie zależności jest niezbędne w przypadku kodu o typie statycznym, takiego jak Java lub C #.
I ... Należy również zauważyć, że obiekt może poprawnie wypełniać swój cel tylko w środowisku wykonawczym, o ile wszystkie jego zależności są dostępne, więc konfigurowanie wstrzykiwania właściwości jest mało przydatne. Moim zdaniem, wszystkie zależności powinny być spełnione, gdy wywoływany jest konstruktor, więc do tego należy się stosować.
Mam nadzieję, że to pomogło.
źródło
Myślę, że klasyczną odpowiedzią jest stworzenie bardziej oddzielonej aplikacji, która nie ma wiedzy o tym, która implementacja zostanie użyta w czasie wykonywania.
Na przykład jesteśmy centralnym dostawcą płatności, współpracującym z wieloma dostawcami płatności na całym świecie. Jednak po złożeniu wniosku nie mam pojęcia, do którego procesora płatności zadzwonię. Mógłbym zaprogramować jedną klasę z mnóstwem obudów przełączników, takich jak:
Teraz wyobraź sobie, że teraz będziesz musiał utrzymywać cały ten kod w jednej klasie, ponieważ nie jest on prawidłowo odsprzęgnięty, możesz sobie wyobrazić, że dla każdego nowego obsługiwanego procesora będziesz musiał utworzyć nowy przypadek przełączania if // dla przy każdej metodzie staje się to jeszcze bardziej skomplikowane, jednak dzięki zastosowaniu Dependency Injection (lub Inversion of Control - jak to się czasami nazywa, co oznacza, że ktokolwiek kontroluje działanie programu, jest znany tylko w czasie wykonywania, a nie komplikacji), możesz coś osiągnąć bardzo schludne i łatwe w utrzymaniu.
** Kod się nie skompiluje, wiem :)
źródło
new ThatProcessor()
Głównym powodem korzystania z DI jest to, że chcesz ponosić odpowiedzialność za wiedzę dotyczącą implementacji tam, gdzie jest ona dostępna. Idea DI jest bardzo zgodna z enkapsulacją i projektowaniem według interfejsu. Jeśli interfejs wymaga od zaplecza podania pewnych danych, to czy nie ma znaczenia dla interfejsu, w jaki sposób zaplecze rozwiązuje to pytanie. To zależy od modułu obsługi zapytań.
To jest już powszechne w OOP od dłuższego czasu. Wiele razy tworzy fragmenty kodu, takie jak:
Wadą jest to, że klasa implementacji jest wciąż zakodowana na stałe, dlatego ma interfejs, który wie, która implementacja jest używana. DI posuwa projekt o krok dalej, że jedyną rzeczą, którą musi wiedzieć front-end, jest znajomość interfejsu. Pomiędzy DYI i DI znajduje się wzorzec lokalizatora usługi, ponieważ interfejs użytkownika musi dostarczyć klucz (obecny w rejestrze lokalizatora usług), aby umożliwić jego rozpatrzenie. Przykład lokalizatora usług:
Przykład DI:
Jednym z wymagań DI jest to, że kontener musi być w stanie dowiedzieć się, która klasa jest implementacją danego interfejsu. Dlatego kontener DI wymaga silnie typowanego projektu i tylko jednej implementacji dla każdego interfejsu w tym samym czasie. Jeśli potrzebujesz więcej implementacji interfejsu w tym samym czasie (jak kalkulator), potrzebujesz lokalizatora usług lub wzorca projektowania fabryki.
D (b) I: Wstrzykiwanie zależności i projektowanie według interfejsu. To ograniczenie nie jest jednak bardzo dużym problemem praktycznym. Zaletą używania D (b) I jest to, że służy on komunikacji między klientem a dostawcą. Interfejs jest perspektywą dla obiektu lub zestawu zachowań. To ostatnie jest tutaj kluczowe.
Wolę administrować umowami o świadczenie usług wraz z kodowaniem D (b) I. Powinny iść razem. Zastosowanie D (b) I jako rozwiązania technicznego bez organizacyjnego zarządzania umowami o świadczenie usług nie jest moim zdaniem bardzo korzystne, ponieważ DI jest wtedy tylko dodatkową warstwą enkapsulacji. Ale jeśli możesz używać go razem z administracją organizacyjną, możesz naprawdę skorzystać z zasady organizacyjnej, którą oferuje D (b) I. Może pomóc w dłuższej perspektywie w ustrukturyzowaniu komunikacji z klientem i innymi działami technicznymi w tematach takich jak testowanie, wersjonowanie i opracowywanie alternatyw. Jeśli masz niejawny interfejs, taki jak w klasie zakodowanej na stałe, jest on z czasem znacznie mniej komunikatywny niż wtedy, gdy wyrazisz go jawnie za pomocą D (b) I. Wszystko sprowadza się do konserwacji, która jest z czasem i nie na raz. :-)
źródło