Главная страница   /   5.2. Bastard Injection (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

5.2. Bastard Injection

Перегруженные конструкторы являются довольно распространенными во многих базах кода .NET (включая BCL). Часто многие перегруженные варианты обеспечивают разумные значения по умолчанию для одного или двух полномасштабных конструкторов, которые принимают все соответствующие параметры в качестве входных данных.

Время от времени мы видим другое использование, когда речь идет о DI. Довольно общий анти-паттерн определяет предназначенный для тестирования перегруженный вариант конструктора, который позволяет нам явно определить зависимость, в то время как конструктор по умолчанию используется в производственном коде.

Это может быть вредным, когда реализация зависимости по умолчанию представляет Foreign Default, а не Local Default.

Когда мы полностью охватываем DI, такие перегруженные конструкторы становятся лишними в лучшем случае. Учитывая негативные последствия, лучше избегать их.

ProductService с Foreign Default

Когда Мэри первоначально реализовала класс ProductService (в главе 2), она имела ввиду только одну зависимость: реализацию на основе SQL Server. Класс SqlProductRepository изначально был задуман как единственная реализация ProductRepository, поэтому казалось очевидным использовать его по умолчанию.

Foreign Default

Foreign Default является противоположностью Local Default. Это реализация зависимости, которая используется по умолчанию, даже если она определена в другом модуле, чем ее потребитель.

В качестве примера давайте рассмотрим реализации Repository, которые мы видели в примере коммерческого приложения в предыдущих главах. Сервис, такой как ProductService, требует для работы экземпляр ProductRepository. Во многих случаях, когда мы разрабатываем такие приложения, мы имеем в виду разумную реализацию: ту, которая реализует нужный функционал путем чтения и записи данных в и из реляционной базы данных. Было бы заманчиво использовать такую реализацию по умолчанию.

Проблема заключается в том, что реализация по умолчанию, которую мы имеем в виду (SqlProductRepository), определена в другом модуле, нежели ProductService. Это заставляет нас принять нежелательную зависимость для модуля CommerceSqlDataAccess, как показано здесь.

Когда ProductService использует SqlProductRepository в качестве реализации по умолчанию, это заставляет нас делать жесткую ссылку на модуль CommerceSqlDataAccess, а мы этого не хотим.

Использование нежелательных модулей отнимает у нас многие преимущества слабой связанности, которые обсуждались в главе 1. Все сложнее становится повторное использование модуля CommerceDomain, потому что он потянет за собой модуль CommerceSqlDataAccess, а мы, возможно, не захотим использовать это в другом контексте. Это также усложняет параллельную разработку, потому что класс ProductService теперь напрямую зависит от класса SqlProductRepository.

Таковы основные причины того, что вы должны избегать Foreign Default, если это вообще возможно.

Мэри еще не чувствует себя комфортно с идеей внедрения в конструктор, потому что у нее есть проблема в выяснении, где будет размещена структура объекта. Она до сих пор не обращала внимания на концепцию Composition Root.

Приглашенный консультант сказал ей использовать внедрение в конструктор для ProductService, но она по-прежнему считает, что она должна создать его новый экземпляр следующим образом:

var productService = new ProductService();

Для этого она добавляет следующий код в класс ProductService.

Листинг 5-2: ProductService с Bastard Injection
private readonly ProductRepository repository;
public ProductService()
	: this(ProductService.CreateDefaultRepository())
{
}
public ProductService(ProductRepository repository)
{
	if (repository == null)
	{
		throw new ArgumentNullException("repository");
	}
	this.repository = repository;
}
private static ProductRepository CreateDefaultRepository()
{
	string connectionString =
		ConfigurationManager.ConnectionStrings
		["CommerceObjectContext"].ConnectionString;
	return new SqlProductRepository(connectionString);
}

Строки 2-5: Конструктор по умолчанию

Строки 6-13: Внедрение в конструктор

Класс ProductService теперь имеет конструктор по умолчанию, который вызывает его другой конструктор, используя Foreign Default.

Другой конструктор правильно реализует паттерн внедрение в конструктор, имея ограждающее условие, а затем сохраняя внедренный ProductRepository в поле только для чтения. Конструктор по умолчанию вызывает этот конструктор с Foreign Default, созданной в закрытом методе CreateDefaultRepository. Класс SqlProductRepository является Foreign Default, поскольку он определен в другой сборке, чем класс ProductService. Это приводит к тому, что сборка, содержащая класс ProductService, тесно связана со сборкой, содержащей класс SqlProductRepository.

Хотя ProductService можно использовать с различными типами ProductRepository, передавая их с помощью самого гибкого перегруженного варианта конструктора, Мэри не сможет перехватить экземпляр ProductRepository в приложении, если она настаивает на использовании конструктора по умолчанию.

Анализ

Bastard Injection наиболее часто встречается, когда разработчики пытаются сделать свои классы тестируемыми без полного понимания DI. При написании модульных тестов для класса очень важно, чтобы мы могли заменить изменчивую зависимость дублирующим тестом, так чтобы мы могли правильно изолировать тестируемую систему (SUT) от ее зависимостей, и внедрение в конструктор позволяет сделать именно это.

Хотя Bastard Injection включает тестируемость, он имеет некоторые нежелательные последствия.

Конкретный пример: ASP.NET MVC

Когда вы создаете новый ASP.NET MVC проект, автоматически создается несколько стандартных классов контроллеров. Одним из них является класс AccountController, который использует Bastard Injection. В исходном коде это объясняется даже в комментариях:

// Этот конструктор используется MVC фреймворк,
// чтобы создать экземпляр контроллера при помощи 
// форм аутентификации по умолчанию и провайдеров членства.
public AccountController()
	: this(null, null)
{
}
// Этот конструктор не используется MVC фреймворком,
// но он используется для упрощения юнит тестирования этого типа.
// Посмотрите комментарии в конце этого файла
// для более полной информации.
public AccountController(IFormsAuthentication formsAuth,
	IMembershipService service)
{
	this.FormsAuth = formsAuth ?? new FormsAuthenticationService();
	this.MembershipService = service ?? new AccountMembershipService();
}

Как я могу сказать, что Bastard Injection – это плохо, когда кажется, что Microsoft использует и одобряет его? В данном случае, мотивация, кажется, исключительно связана с тестируемостью, и Bastard Injection вполне соответствует этой цели – он просто не соответствует другим целям модульности, таким как способность к замещению и повторному использованию модулей, а также параллельной разработке.

Другие придерживаются того же мнения. Айенде Райен отметил следующее в своем блоге, в котором обсуждалось ASP.NET MVC приложение:

Я имею в виду, если вы хотите сделать слабый IoC – вперед. Но, пожалуйста, не создавайте этого бастарда.

Эта фраза вдохновила меня назвать анти-паттерн так, как я это сделал.

Влияние

Основная проблема с Bastard Injection заключается в использовании Foreign Default. Хотя тестируемость и включена, мы больше не можем свободно повторно использовать класс, потому что это потянет за собой зависимость, которую мы не хотим. Кроме того, параллельная разработка усложняется, потому что класс сильно зависит от своей зависимости.

В дополнение к последствиям Bastard Injection для модульности приложения, существование множества конструкторов также представляет собой другой тип проблемы. Когда есть только один конструктор, DI контейнер может автоматически проводить все зависимости, потому что никогда не встает вопрос, какой конструктор использовать.

Когда существует более чем один конструктор, выбор между конструкторами становится неоднозначным. DI контейнеру приходится прибегать к своего рода эвристике, чтобы сделать выбор между различными конструкторами, или он может от этого отказаться. Как ни странно, это один из немногих сценариев, где бедное DI менее подвержено влиянию, потому что мы можем решать в каждом конкретном случае индивидуально, когда мы вручную проводим зависимости.

Среди различных анти-паттернов DI, Bastard Injection не так вреден, как Control Freak, но и от него также гораздо легче избавиться.

Рефакторинг по направлению к DI

Bastard Injection часто является результатом ошибочной попытки реализовать DI. Хорошо, что есть такие основы, как программирование к интерфейсам, так что провести рефакторинг к надлежащему DI паттерну легко.

Совет

Даже если вы думаете, что воздействие Bastard Injection вас не касается, вы все равно должны провести рефакторинг к надлежащему DI паттерну. Ведь это так просто сделать, что не стоит даже сомневаться в нужности этого.

Первый шаг заключается в выборе того, какой DI паттерн соответствует цели. Рисунок 5-3 иллюстрирует простой процесс принятия решений. Если значение по умолчанию, которые было использовано до сих пор, это Foreign Default, лучшим выбором является внедрение в конструктор. В другом случае, хорошей альтернативой считается внедрение в свойство.

Рисунок 5-3: При рефакторинге от Bastard Injection решающий фактор заключается в том, является ли зависимость Foreign Default или Local Default.

Во многих случаях, значение по умолчанию, используемое конструктором по умолчанию, представляет Foreign Default. В этих случаях внедрение в конструктор является лучшим ответом, потому что он так прост в реализации и хорошо справляется с любыми видами зависимостей. Конструктор, который принимает зависимость в качестве параметра, уже существует, так что единственное изменение, которое мы должны сделать в используемом классе, заключается в удалении конструктора по умолчанию.

Это, несомненно, приведет к некоторым ошибкам компилятора, но на данный момент мы можем опереться на компилятор и переместить весь код, который создает рассматриваемый класс, в Composition Root.

Когда значение по умолчанию представляет Local Default, ситуация очень напоминает основной сценарий для внедрения в свойство. Хотя механика и разная, структура такая же: в обоих случаях у нас есть подходящая Local Default, но мы все еще хотим открыть наш потребляющий класс для расширяемости.

Это вырождающийся случай Bastard Injection, где воздействие гораздо менее серьезное. Поскольку значение по умолчанию является Local Default, нет никакого влияния на степень компонуемости класса; единственным негативным последствием является то, что двусмысленность конструктора делает автовнедрение более сложным.

В данном случае мы могли бы реализовать внедрение в свойство, удалив конструктор, который принимает зависимость в качестве параметра, и заменив его свойством для записи. Если это изменение приводит к ошибкам компилятора, мы снова можем опереться на компилятор и переместить создание кода в Composition Root.

В Composition Root есть много различных способов компоновать зависимости, в том числе такие, которые далеки от идеальных, как покажет следующий анти-паттерн.