Что такое инъекция зависимостей (DI)?
Как уже говорили другие, Dependency Injection (DI) устраняет ответственность за непосредственное создание и управление продолжительностью жизни других экземпляров объекта, от которых зависит наш класс интересов (потребительский класс) (в смысле UML ). Эти экземпляры вместо этого передаются в наш потребительский класс, как правило, в качестве параметров конструктора или через установщики свойств (управление экземпляром объекта зависимости и передачей в потребительский класс обычно выполняется с помощью Inversion of Control (IoC) контейнером , но это другая тема) ,
DI, DIP и SOLID
В частности, в парадигме SOLID принципов Роберта С. Мартина объектно-ориентированного проектирования , DIявляется одной из возможных реализаций принципа инверсии зависимости (DIP) . DIP является Dв SOLIDмантре - другие реализации DIP включают Service Locator, и паттерны Plugin.
Целью DIP является разделение плотно, конкретные зависимости между классами, и вместо того , чтобы ослабить муфту с помощью абстракции, которые могут быть достигнуты с помощью interface, abstract classилиpure virtual class , в зависимости от используемого языка и подхода.
Без DIP наш код (я назвал это «потребляющий класс») напрямую связан с конкретной зависимостью и также часто обременен обязанностью знать, как получить и управлять экземпляром этой зависимости, то есть концептуально:
"I need to create/use a Foo and invoke method `GetBar()`"
Принимая во внимание, что после применения DIP требование ослабляется, и Fooустраняется проблема получения и управления сроком службы зависимости:
"I need to invoke something which offers `GetBar()`"
Зачем использовать DIP (и DI)?
Разделение зависимостей между классами таким способом позволяет легко заменить эти классы зависимостей другими реализациями, которые также выполняют предварительные условия абстракции (например, зависимость может быть переключена с другой реализацией того же интерфейса). Кроме того, как уже упоминалось, возможно , , наиболее распространенной причиной для классов разъединить с помощью DIP, чтобы позволить потребляя класс для тестирования в изоляции, так как эти же зависимостей теперь могут быть загасил и / или издевались.
Одним из следствий DI является то, что управление временем жизни экземпляров объекта зависимости больше не контролируется потребляющим классом, так как объект зависимости теперь передается в потребляющий класс (через внедрение конструктора или сеттера).
Это можно посмотреть по-разному:
- Если необходимо сохранить контроль продолжительности жизни класса-потребителя, контроль можно восстановить, введя (абстрактную) фабрику для создания экземпляров класса зависимости в класс-потребитель. Потребитель сможет получать экземпляры через
Createфабрику по мере необходимости и утилизировать эти экземпляры после завершения.
- Или управление продолжительностью жизни экземпляров зависимостей может быть передано контейнеру IoC (подробнее об этом ниже).
Когда использовать DI?
- Там, где, вероятно, возникнет необходимость заменить зависимость эквивалентной реализацией,
- В любое время, когда вам нужно будет тестировать методы класса изолированно от его зависимостей,
- Там, где неопределенность продолжительности жизни зависимости может потребовать экспериментов (например, «Эй,
MyDepClassэто потокобезопасность?
пример
Вот простая реализация C #. Учитывая ниже класс потребления:
public class MyLogger
{
public void LogRecord(string somethingToLog)
{
Console.WriteLine("{0:HH:mm:ss} - {1}", DateTime.Now, somethingToLog);
}
}
Несмотря на то, что он выглядит безобидным, он имеет две staticзависимости от двух других классов System.DateTimeи System.Console, который не только ограничивает параметры вывода журналов (вход в консоль будет бесполезным, если никто не наблюдает), но, что еще хуже, автоматическое тестирование с учетом зависимости от этого затруднительно недетерминированные системные часы.
Однако мы можем применить DIPк этому классу, абстрагируясь от того, что временные метки являются зависимостями и связаны MyLoggerтолько с простым интерфейсом:
public interface IClock
{
DateTime Now { get; }
}
Мы также можем ослабить зависимость от Consoleабстракции, такой как TextWriter. Внедрение зависимостей обычно реализуется либо как constructorвнедрение (передача абстракции зависимости в качестве параметра конструктору потребляющего класса), либо Setter Injection(передача зависимости через setXyz()установщик или свойство .Net с {set;}заданным значением). Внедрение в конструктор является предпочтительным, поскольку это гарантирует, что класс будет в правильном состоянии после создания, и позволяет полям внутренней зависимости помечаться как readonly(C #) или final(Java). Таким образом, используя инъекцию конструктора в приведенном выше примере, мы получаем:
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);
}
}
(Необходимо предоставить конкретную Clockинформацию, к которой, конечно, можно вернуться DateTime.Now, и две зависимости должны быть предоставлены контейнером IoC посредством внедрения конструктора)
Может быть построен автоматический модульный тест, который однозначно доказывает, что наш регистратор работает правильно, поскольку теперь у нас есть контроль над зависимостями - временем, и мы можем следить за записанным выводом:
[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());
}
Следующие шаги
Внедрение зависимостей неизменно связано с контейнером инверсии (IoC) , чтобы внедрить (предоставить) конкретные экземпляры зависимостей и управлять экземплярами продолжительности жизни. В процессе конфигурирования / начальной загрузки IoCконтейнеры позволяют определять следующее:
- отображение между каждой абстракцией и сконфигурированной конкретной реализацией (например, «всякий раз, когда потребитель запрашивает
IBar, возвращает ConcreteBarэкземпляр» )
- Политики могут быть настроены для управления продолжительностью жизни каждой зависимости, например, для создания нового объекта для каждого экземпляра-потребителя, для совместного использования экземпляра одноэлементной зависимости для всех потребителей, для совместного использования одного и того же экземпляра зависимости только в одном потоке и т. д.
- В .Net контейнеры IoC знают о таких протоколах, как
IDisposableи будут нести ответственность за Disposingзависимости в соответствии с настроенным управлением продолжительностью жизни.
Как правило, после настройки / загрузки контейнеров IoC они работают в фоновом режиме, позволяя кодеру сосредоточиться на имеющемся коде, а не беспокоиться о зависимостях.
Ключ к DI-дружественному коду заключается в том, чтобы избежать статического связывания классов и не использовать new () для создания зависимостей.
Как в приведенном выше примере, разделение зависимостей требует определенных усилий по проектированию, и для разработчика существует смена парадигмы, необходимая для того, чтобы избавиться от привычки newнапрямую зависеть от зависимостей и вместо этого доверять контейнеру управление зависимостями.
Но преимуществ много, особенно в том, что касается возможности тщательно проверить свой класс интересов.
Примечание . Создание / отображение / проекция (через new ..()) POCO / POJO / Сериализация DTO / Графы сущностей / Анонимные проекции JSON и др., Т.е. классы или записи «Только данные», используемые или возвращаемые из методов, не рассматриваются как зависимости (в Смысл UML) и не подлежит DI. Использование newдля проецирования это просто отлично.