Wstrzykiwanie zależności (DI) nie jest koncepcją łatwą do zrozumienia, a zastosowanie jej do nowych lub istniejących aplikacji jest jeszcze bardziej zagmatwane. Jess Smith pokazuje, jak wykonać zastrzyk zależności bez kontenera wstrzykiwania w językach programowania C# i Java. W tym artykule pokażę, jak zaimplementować wstrzykiwanie zależności (DI) w aplikacjach .NET i Java. Koncepcja wstrzykiwania zależności po raz pierwszy zwróciła uwagę programistów w 2000 roku, kiedy Robert Martin napisał artykuł „Zasady i wzorce projektowe” (później znany pod akronimem
SOLID ). Litera D w SOLID odnosi się do zależności inwersji (DOI), która później stała się znana jako wstrzykiwanie zależności. Oryginalna i najczęstsza definicja: inwersja zależności to odwrócenie sposobu, w jaki klasa bazowa zarządza zależnościami. W oryginalnym artykule Martina wykorzystano następujący kod, aby zilustrować zależność klasy
Copy
od klasy niższego poziomu
WritePrinter
:
void Copy()
{
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
Pierwszym oczywistym problemem jest to, że jeśli zmienisz listę parametrów lub typy metody
WritePrinter
, musisz zaimplementować aktualizacje wszędzie tam, gdzie istnieje zależność od tej metody. Proces ten zwiększa koszty utrzymania i jest potencjalnym źródłem nowych błędów.
Kolejny problem: klasa Copy nie jest już potencjalnym kandydatem do ponownego użycia. Na przykład, co się stanie, jeśli chcesz przesłać znaki wprowadzone z klawiatury do pliku, a nie do drukarki? Aby to zrobić, możesz zmodyfikować klasę
Copy
w następujący sposób (składnia języka C++):
void Copy(outputDevice dev)
{
int c;
while ((c = ReadKeyboard()) != EOF)
if (dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
Pomimo wprowadzenia nowej zależności
WriteDisk
sytuacja nie poprawiła się (a raczej pogorszyła), gdyż naruszona została kolejna zasada: „byty programowe, czyli klasy, moduły, funkcje itd. powinny być otwarte na rozbudowę, ale zamknięte na modyfikacja." Martin wyjaśnia, że te nowe instrukcje warunkowe if/else zmniejszają stabilność i elastyczność kodu. Rozwiązaniem jest odwrócenie zależności, tak aby metody zapisu i odczytu zależały od
Copy
. Zamiast „przepychać” zależności, są one przekazywane przez konstruktor. Zmodyfikowany kod wygląda następująco:
class Reader
{
public:
virtual int Read() = 0;
};
class Writer
{
public:
virtual void Write(char) = 0;
};
void Copy(Reader& r, Writer& w)
{
int c;
while((c=r.Read()) != EOF)
w.Write(c);
}
Teraz klasę
Copy
można łatwo ponownie wykorzystać z różnymi implementacjami metod klasowych
Reader
i
Writer
. Klasa
Copy
nie posiada żadnych informacji o wewnętrznej strukturze typów
Reader
i
Writer
, co umożliwia ich ponowne wykorzystanie w różnych implementacjach. Ale jeśli to wszystko wydaje ci się jakimś bełkotem, być może poniższe przykłady w Javie i C# wyjaśnią sytuację.
Przykład w Javie i C#
Aby zilustrować łatwość wstrzykiwania zależności bez kontenera zależności, zacznijmy od prostego przykładu, który można dostosować do użycia
DI
w zaledwie kilku krokach. Załóżmy, że mamy klasę
HtmlUserPresentation
, która po wywołaniu jej metod generuje interfejs użytkownika HTML. Oto prosty przykład:
HtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Każdy projekt wykorzystujący ten kod klasy będzie zależny od klasy
HtmlUserPresentation
, co spowoduje opisane powyżej problemy z użytecznością i konserwacją. Od razu nasuwa się usprawnienie: utworzenie interfejsu z sygnaturami wszystkich metod aktualnie dostępnych w klasie
HtmlUserPresentation
. Oto przykład tego interfejsu:
public interface IHtmlUserPresentation {
String createTable(ArrayList rowVals, String caption);
String createTableRow(String tableCol);
}
Po utworzeniu interfejsu modyfikujemy klasę tak,
HtmlUserPresentation
aby z niego korzystała. Wracając do tworzenia instancji typu
HtmlUserPresentation
, możemy teraz użyć typu interfejsu zamiast typu podstawowego:
IHtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Stworzenie interfejsu pozwala nam łatwo korzystać z innych implementacji
IHtmlUserPresentation
. Przykładowo, jeśli chcemy przetestować ten typ, możemy łatwo zastąpić typ podstawowy
HtmlUserPresentation
innym typem o nazwie
HtmlUserPresentationTest
. Wprowadzone do tej pory zmiany ułatwiają testowanie, konserwację i skalowanie kodu, ale nie powodują ponownego wykorzystania, ponieważ wszystkie
HtmlUserPresentation
klasy korzystające z tego typu są nadal świadome jego istnienia. Aby usunąć tę bezpośrednią zależność, możesz przekazać typ interfejsu
IHtmlUserPresentation
do konstruktora (lub listy parametrów metody) klasy lub metody, która będzie go używać:
public UploadFile(IHtmlUserPresentation htmlUserPresentation)
Konstruktor
UploadFile
ma teraz dostęp do wszystkich funkcjonalności typu
IHtmlUserPresentation
, ale nie wie nic o wewnętrznej strukturze klasy, która implementuje ten interfejs. W tym kontekście wstrzykiwanie typu ma miejsce, gdy tworzona jest instancja klasy
UploadFile
. Typ interfejsu
IHtmlUserPresentation
staje się możliwy do ponownego użycia poprzez przekazanie różnych implementacji do różnych klas lub metod, które wymagają różnych funkcjonalności.
Wnioski i zalecenia dotyczące konsolidacji materiału
Dowiedziałeś się o wstrzykiwaniu zależności i że mówi się, że klasy bezpośrednio zależą od siebie, gdy jedna z nich tworzy instancję drugiej, aby uzyskać dostęp do funkcjonalności typu docelowego. Aby oddzielić bezpośrednią zależność między tymi dwoma typami, należy utworzyć interfejs. Interfejs daje typowi możliwość dołączania różnych implementacji, w zależności od kontekstu wymaganej funkcjonalności. Przekazując typ interfejsu do konstruktora klasy lub metody, klasa/metoda potrzebująca danej funkcjonalności nie zna żadnych szczegółów na temat typu implementującego interfejs. Z tego powodu typ interfejsu może być ponownie używany w różnych klasach, które wymagają podobnego, ale nie identycznego zachowania.
- Aby poeksperymentować ze wstrzykiwaniem zależności, spójrz na swój kod z jednej lub większej liczby aplikacji i spróbuj przekonwertować często używany typ podstawowy na interfejs.
- Zmień klasy, które bezpośrednio tworzą instancję tego typu podstawowego, aby korzystały z nowego typu interfejsu, i przekaż go przez konstruktor lub listę parametrów metody klasy, która będzie go używać.
- Utwórz implementację testową, aby przetestować ten typ interfejsu. Po refaktoryzacji kod
DI
stanie się łatwiejszy do wdrożenia i zauważysz, o ile bardziej elastyczna stanie się Twoja aplikacja pod względem ponownego użycia i łatwości konserwacji.
GO TO FULL VERSION