Внедрение зависимостей или инъекция зависимостей (Dependency injection, DI) – непростая для понимания концепция, а её применение к новым или уже существующим приложениям – задача еще более запутанная. Джесс Смит покажет вам, как осуществлять внедрение зависимостей без контейнера внедрения на языках программирования C# и Java.
![Простой способ внедрения зависимостей - 1]()
В этой статье я покажу вам, как внедрять зависимости (DI) в .NET- и Java-приложениях. Концепция внедрения зависимостей впервые появилась в поле зрения разработчиков в 2000 году, когда Роберт Мартин написал статью "Принципы и паттерны проектирования" (позднее получивших известность под аббревиатурой
SOLID). Буква D в SOLID относится к инверсии зависимостей (Dependency of Inversion, DOI), которую позднее стали называть внедрением зависимостей.
Изначальное и чаще всего встречающееся определение: инверсия зависимостей — это инверсия способа управления зависимостями базовым классом. В исходной статье Мартина использовался следующий код, иллюстрирующий зависимость класса
Copy
от более низкоуровневого класса
WritePrinter
:
void Copy()
{
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
Первая очевидная проблема: если изменить список или типы параметров метода
WritePrinter
, нужно внедрить обновления везде, где есть зависимость от этого метода. Этот процесс повышает затраты на обслуживание и является потенциальным источником новых ошибок.
Другая проблема: класс Copy перестает быть потенциальным кандидатом на повторное использование. Например, что делать, если вам понадобится вывести вводимые с клавиатуры символы в файл вместо принтера? Для этого можно модифицировать класс
Copy
следующим образом (синтаксис языка C++):
void Copy(outputDevice dev)
{
int c;
while ((c = ReadKeyboard()) != EOF)
if (dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
Несмотря на появление новой зависимости
WriteDisk
, ситуация не улучшилась (а скорее ухудшилась), поскольку был нарушен другой принцип: "программные сущности, то есть, классы, модули, функции и так далее, должны быть открыты для расширения, но закрыты для изменения".
Мартин поясняет, что эти новые условные операторы if/else понижают стабильность и гибкость кода. Решение состоит в инверсии зависимостей, чтобы методы записи и чтения зависели от класса
Copy
. Вместо "выталкивания" зависимостей, они передаются через конструктор.
Переделанный код выглядит следующим образом:
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);
}
Теперь класс
Copy
можно легко использовать повторно с различными реализациями методов классов
Reader
и
Writer
. У класса
Copy
нет никакой информации о внутреннем устройстве типов
Reader
и
Writer
, благодаря чему возможно их переиспользование с различными реализациями.
Но если всё это кажется вам какой-то абракадаброй, возможно, ситуацию прояснят приведенные ниже примеры на языках Java и C#.
Пример на языках Java и C#
Для иллюстрации простоты внедрения зависимостей без контейнера зависимостей, начнем с простого примера, который можно переделать под использование
DI
всего за несколько шагов.
Допустим, у нас есть класс
HtmlUserPresentation
, который, при вызове его методов, формирует пользовательский HTML-интерфейс.
Вот простой пример:
HtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
У любого использующего этот код класса проекта появляется зависимость от класса
HtmlUserPresentation
, что приводит к вышеописанным проблемам с удобством использования и обслуживанием.
Сразу напрашивается усовершенствование: создание интерфейса с сигнатурами всех ныне имеющихся в классе
HtmlUserPresentation
методов.
Вот пример этого интерфейса:
public interface IHtmlUserPresentation {
String createTable(ArrayList rowVals, String caption);
String createTableRow(String tableCol);
// Оставшиеся сигнатуры
}
После создания интерфейса, модифицируем класс
HtmlUserPresentation
для его использования. Возвращаясь к созданию экземпляра типа
HtmlUserPresentation
, мы можем теперь использовать тип интерфейса вместо базового:
IHtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Создание интерфейса позволяет нам легко использовать другие реализации типа
IHtmlUserPresentation
. Например, если мы хотим протестировать этот тип, то легко можем заменить базовый тип
HtmlUserPresentation
на другой тип, под названием
HtmlUserPresentationTest
.
Выполненные до сих пор изменения упрощают тестирование, обслуживание и масштабирование кода, но ничего не делают для переиспользования, поскольку все использующие тип
HtmlUserPresentation
классы все еще знают о его существовании.
Чтобы убрать эту прямую зависимость, можно передавать интерфейсный тип
IHtmlUserPresentation
в конструктор (или список параметров метода) класса или метод, который его будет использовать:
public UploadFile(IHtmlUserPresentation htmlUserPresentation)
У конструктора
UploadFile
теперь есть доступ ко всей функциональности типа
IHtmlUserPresentation
, но он ничего не знает о внутреннем устройстве реализующего этот интерфейс класса.
В данном контексте, внедрение типа происходит при создании экземпляра класса
UploadFile
. Интерфейсный тип
IHtmlUserPresentation
становится переиспользуемым, передавая различные реализации различным классам или методам, для которых необходима разная функциональность.
Заключение и рекомендации для закрепления материала
Вы узнали о том, что такое внедрение зависимостей и о том, что классы называются напрямую зависящими друг от друга тогда, когда один из них создает экземпляр другого для получения доступа к функциональности целевого типа.
Для расцепления прямой зависимости между двумя типами следует создать интерфейс. Интерфейс предоставляет типу возможность включать различные реализации, в зависимости от контекста необходимой функциональности.
Благодаря передаче интерфейсного типа конструктору или методу класса, класс/метод, для которого нужна функциональность, не знает никаких подробностей о реализующем интерфейс типе. В силу этого интерфейсный тип можно использовать повторно для различных классов, требующих схожего, но не одинакового поведения.
- Чтобы поэкспериментировать с внедрением зависимостей, просмотрите свой код из одного или нескольких приложений и попробуйте переделать интенсивно используемый базовый тип в интерфейс.
- Измените непосредственно создающие экземпляры этого базового типа классы так, чтобы они использовали этот новый интерфейсный тип и передавали его через конструктор или список параметров метода класса, который его должен будет использовать.
- Создайте тестовую реализацию для проверки этого интерфейсного типа. После рефакторинга вашего кода реализовать
DI
станет проще, и вы заметите, насколько более гибким станет ваше приложение в смысле переиспользования и сопровождения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ