Что такое инъекция зависимостей (DI)?
Как уже говорили другие, Внедрение зависимостей (DI) устраняет ответственность прямого создания и управления продолжительностью жизни других экземпляров объекта, от которых зависит наш класс интересов (потребительский класс) (в смысл UML ). Эти экземпляры вместо этого передаются в наш потребительский класс, как правило, в качестве параметров конструктора или через установщики свойств (управление экземпляром объекта зависимости и передачей в потребительский класс обычно выполняется контейнером Inversion of Control (IoC) ). , но это уже другая тема).
DI, DIP и SOLID
В частности, в парадигме Роберта С. Мартина SOLID принципы объектно-ориентированного проектирования , DI
является одной из возможных реализаций принципа обращения зависимостей (DIP) . DIP - это D
SOLID
мантры - другие реализации DIP включают в себя Service Locator и шаблоны плагинов.
Цель 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
является поточно-ориентированным - что если мы сделаем его одиночным и добавим один и тот же экземпляр всем потребителям?)
* ** 1074 тысяча семьдесят три * Пример * * тысяча семьдесят шесть
Вот простая реализация на 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
для проецирования это просто замечательно.