Co to jest wstrzykiwanie zależności (DI)?
Jak powiedzieli inni, Dependency Injection (DI) usuwa odpowiedzialność za bezpośrednie tworzenie i zarządzanie długością życia, innych obiektów obiektowych, od których zależy nasza klasa zainteresowań (klasa konsumentów) (w sensie UML ). Te instancje są zamiast tego przekazywane do naszej klasy konsumenta, zwykle jako parametry konstruktora lub za pomocą ustawiaczy właściwości (zarządzanie instancją obiektu zależności i przekazywaniem do klasy konsumenta jest zwykle wykonywane przez kontener Inversion of Control (IoC) , ale to inny temat) .
DI, DIP i SOLID
W szczególności, w paradygmacie Roberta C Martina SOLID zasad Object Oriented projekt , DI
jest jednym z możliwych implementacji Dependency Inversion Principle (DIP) . DIP jest D
z SOLID
mantrą - inne implementacje DIP obejmują lokalizatora usług i wzorców wtyczki.
Celem DIP jest oddzielenie szczelne, betonowe zależności między klasami, a zamiast tego, aby poluzować sprzęgło za pomocą abstrakcji, co można osiągnąć poprzez interface
, abstract class
lub pure virtual class
, w zależności od języka i podejścia stosowanego.
Bez DIP nasz kod (nazwałam tę „klasą konsumpcyjną”) jest bezpośrednio połączony z konkretną zależnością, a także często jest obciążony odpowiedzialnością za uzyskanie i zarządzanie instancją tej zależności, tj. Koncepcyjnie:
"I need to create/use a Foo and invoke method `GetBar()`"
Podczas gdy po zastosowaniu DIP wymóg został rozluźniony, a problem uzyskiwania i zarządzania Foo
czasem trwania zależności został usunięty:
"I need to invoke something which offers `GetBar()`"
Dlaczego warto korzystać z DIP (i DI)?
Oddzielenie zależności między klasami w ten sposób pozwala na łatwe zastąpienie tych klas zależności innymi implementacjami, które również spełniają warunki abstrakcji (np. Zależność można przełączać za pomocą innej implementacji tego samego interfejsu). Ponadto, jak wspominają inni, ewentualnie Najczęstszym powodem do klas oddzielić poprzez DIP jest umożliwienie spożywania klasy należy badać w izolacji, ponieważ te same zależności mogą być teraz zgaszone i / lub wyśmiewany.
Jedną z konsekwencji DI jest to, że zarządzanie żywotnością instancji obiektów zależności nie jest już kontrolowane przez konsumującą klasę, ponieważ obiekt zależności jest teraz przekazywany do konsumującej klasy (poprzez wstrzyknięcie konstruktora lub ustawiacza).
Można to zobaczyć na różne sposoby:
- Jeśli zachowana musi zostać kontrola zależności przez klasę konsumpcji przez całe życie, kontrolę można przywrócić poprzez wstrzyknięcie (abstrakcyjnej) fabryki do tworzenia instancji klasy zależności do klasy konsumenta. Konsument będzie mógł uzyskać instancje
Create
w fabryce w razie potrzeby i zlikwidować je po zakończeniu.
- Lub kontrolę życia instancji zależności można zrezygnować z kontenera IoC (więcej na ten temat poniżej).
Kiedy stosować DI?
- Tam, gdzie prawdopodobnie zajdzie potrzeba zastąpienia zależności równoważnym wdrożeniem,
- Za każdym razem, gdy będziesz musiał przetestować metody klasy w oderwaniu od jej zależności,
- Gdzie niepewność okresu istnienia zależności może uzasadniać eksperymenty (np. Hej,
MyDepClass
czy wątek jest bezpieczny - co, jeśli zrobimy z niego singleton i wstrzykniemy ten sam przypadek wszystkim konsumentom?)
Przykład
Oto prosta implementacja w języku C #. Biorąc pod uwagę poniższą klasę konsumpcyjną:
public class MyLogger
{
public void LogRecord(string somethingToLog)
{
Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
}
}
Choć z pozoru nieszkodliwy, ma dwie static
zależności od dwóch innych klas System.DateTime
i System.Console
, co nie tylko ogranicza opcje wyjściowe rejestrowania (logowanie do konsoli będzie bezwartościowe, jeśli nikt nie patrzy), ale co gorsza, trudno jest automatycznie przetestować, biorąc pod uwagę zależność od niedeterministyczny zegar systemowy.
Możemy jednak zastosować się DIP
do tej klasy, wyodrębniając obawę związaną z oznaczaniem czasu jako zależnością i łącząc MyLogger
tylko z prostym interfejsem:
public interface IClock
{
DateTime Now { get; }
}
Możemy również rozluźnić zależność od Console
abstrakcji, takiej jak a TextWriter
. Wstrzykiwanie zależności jest zwykle realizowane albo jako constructor
wstrzyknięcie (przekazanie abstrakcji do zależności jako parametru do konstruktora klasy konsumującej) lub Setter Injection
(przekazanie zależności przez setXyz()
ustawiacz lub właściwość .Net ze {set;}
zdefiniowaną). Preferowany jest Constructor Injection, ponieważ gwarantuje to, że klasa będzie w poprawnym stanie po zbudowaniu i pozwoli na oznaczenie wewnętrznych pól zależności jako readonly
(C #) lub final
(Java). Tak więc używając zastrzyku konstruktora w powyższym przykładzie, pozostawia nam to:
public class MyLogger : ILogger // Others will depend on our logger.
{
private readonly TextWriter _output;
private readonly IClock _clock;
// Dependencies are injected through the constructor
public MyLogger(TextWriter stream, IClock clock)
{
_output = stream;
_clock = clock;
}
public void LogRecord(string somethingToLog)
{
// We can now use our dependencies through the abstraction
// and without knowledge of the lifespans of the dependencies
_output.Write("{0:yyyy-MM-dd HH:mm:ss} - {1}", _clock.Now, somethingToLog);
}
}
(Należy Clock
podać konkret, do którego można oczywiście wrócić DateTime.Now
, a dwie zależności muszą być zapewnione przez kontener IoC poprzez wstrzyknięcie konstruktora)
Można zbudować automatyczny test jednostkowy, który ostatecznie dowodzi, że nasz rejestrator działa poprawnie, ponieważ teraz mamy kontrolę nad zależnościami - czasem i możemy szpiegować zapisywane dane wyjściowe:
[Test]
public void LoggingMustRecordAllInformationAndStampTheTime()
{
// Arrange
var mockClock = new Mock<IClock>();
mockClock.Setup(c => c.Now).Returns(new DateTime(2015, 4, 11, 12, 31, 45));
var fakeConsole = new StringWriter();
// Act
new MyLogger(fakeConsole, mockClock.Object)
.LogRecord("Foo");
// Assert
Assert.AreEqual("2015-04-11 12:31:45 - Foo", fakeConsole.ToString());
}
Następne kroki
Wstrzykiwanie zależności jest niezmiennie związane z kontenerem Inwersji Kontroli (IoC) , aby wstrzykiwać (udostępniać) konkretne wystąpienia zależności i zarządzać instancjami długości życia. Podczas procesu konfiguracji / ładowania początkowego IoC
kontenery umożliwiają zdefiniowanie następujących elementów:
- mapowanie między każdą abstrakcją a skonfigurowaną konkretną implementacją (np. „za każdym razem, gdy konsument żąda
IBar
, zwraca ConcreteBar
instancję” )
- można skonfigurować zasady zarządzania długością życia każdej zależności, np. w celu utworzenia nowego obiektu dla każdej instancji konsumenta, współużytkowania pojedynczej instancji zależności dla wszystkich konsumentów, udostępniania tej samej instancji zależności tylko w tym samym wątku itp.
- W .Net kontenery IoC są świadome protokołów, takich jak
IDisposable
i przyjmują odpowiedzialność za Disposing
zależności zgodnie ze skonfigurowanym zarządzaniem żywotnością.
Zazwyczaj po skonfigurowaniu / załadowaniu kontenerów IoC działają one płynnie w tle, umożliwiając koderowi skupienie się na dostępnym kodzie zamiast martwienia się o zależności.
Kluczem do kodu przyjaznego DI jest unikanie statycznego łączenia klas, a nie używanie new () do tworzenia zależności
Jak w powyższym przykładzie, odsprzężenie zależności wymaga pewnego wysiłku projektowego, a dla dewelopera konieczna jest zmiana paradygmatu, aby przełamać nawyk new
bezpośredniego uzgadniania zależności i zamiast tego ufać kontenerowi w zarządzaniu zależnościami.
Ale korzyści jest wiele, zwłaszcza możliwość dokładnego przetestowania klasy zainteresowań.
Uwaga : Tworzenie / mapowanie / projekcja (via new ..()
) POCO / POJO / Serialization DTOs / Entity Graphs / Anonymous JSON prognoz i in. - tj. Klasy lub rekordy „Tylko dane” - używane lub zwracane z metod nie są uważane za Zależności (w UML sense) i nie podlega DI. Używanie new
do wyświetlania jest w porządku.