Внедрение зависимостей или инъекция зависимостей (Dependency injection, DI) – непростая для понимания концепция, а её применение к новым или уже существующим приложениям – задача еще более запутанная. Джесс Смит покажет вам, как осуществлять внедрение зависимостей без контейнера внедрения на языках программирования C# и Java.
В этой статье я покажу вам, как внедрять зависимости (DI) в .NET- и Java-приложениях. Концепция внедрения зависимостей впервые появилась в поле зрения разработчиков в 2000 году, когда Роберт Мартин написал статью "Принципы и паттерны проектирования" (позднее получивших известность под аббревиатурой SOLID). Буква D в SOLID относится к инверсии зависимостей (Dependency of Inversion, DOI), которую позднее стали называть внедрением зависимостей. Изначальное и чаще всего встречающееся определение: инверсия зависимостей — это инверсия способа управления зависимостями базовым классом. В исходной статье Мартина использовался следующий код, иллюстрирующий зависимость класса Copy от более низкоуровневого класса WritePrinter:
void Copy()
	{
	 int c;
	 while ((c = ReadKeyboard()) != EOF)
		WritePrinter(c);
	}
Первая очевидная проблема: если изменить список или типы параметров метода WritePrinter, нужно внедрить обновления везде, где есть зависимость от этого метода. Этот процесс повышает затраты на обслуживание и является потенциальным источником новых ошибок.
Интересно читать о Java? Вступайте в группу Java Developer!
Другая проблема: класс 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 станет проще, и вы заметите, насколько более гибким станет ваше приложение в смысле переиспользования и сопровождения.
Что еще почитать?

Сказ о двух итераторах: стратегии конкурентной модификации в Java

Машинный код и байт код: на каком языке говорит ваша программа?