Что такое инъекция зависимостей (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
для проецирования это просто отлично.