Po pierwsze, chcę wyjaśnić założenie, że przyjmę tę odpowiedź. Nie zawsze jest to prawda, ale dość często:
Interfejsy są przymiotnikami; klasy są rzeczownikami.
(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
lub IPrintable
. Klasa jest faktyczną implementacją jednego lub więcej z tych interfejsów: List
lub Map
obie mogą być implementacjami IEnumerable
.
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ępnie Database
zależność od niej Logger
.
Na razie w porządku.
Możesz modelować tę zależność w swojej Database
klasie za pomocą następującego wiersza:
var logger = new Logger();
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
var logger = new Logger();
przez:
var logger = new TcpLogger();
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:
ICanLog logger = new Logger();
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:
ICanLog logger = LoggerFactory.Create();
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:
ICanLog logger = TypeFactory.Create<ICanLog>();
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 typu ICanLog
:
public Database(ICanLog logger) { ... }
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 wymaga ICanPersistData
implementacji, instancja Database
jest wstrzykiwana - ale w tym celu musi najpierw utworzyć instancję tego rodzaju programu rejestrującego, dla którego jest skonfigurowana ICanLog
. 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.