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

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

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

Марк Симан

4.3. Внедрение в метод (Method Injection)

Как мы можем внедрить зависимость в класс, когда она различается для каждой операции?

Передавая ее как параметр метода.

Рисунок 4-7: Клиент создает экземпляр SomeClass, но сначала внедряет экземпляр зависимости ISomeInterface с каждым вызовом метода.

Когда зависимость может меняться с каждым вызовом метода, вы можете передать ее через параметр метода.

Как это работает

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

public void DoStuff(ISomeInterface dependency)

Часто зависимость будет представлять некоторый контекст для операции, который поставляется вместе с "правильным" значением:

public string DoStuff(SomeValue value, ISomeContext context)

В данном случае параметр value представляет собой значение, над которым должен работать метод, тогда как context содержит информацию о текущем контексте операции. Вызывающий элемент внедряет зависимость в метод, а метод использует или игнорирует зависимость, в зависимости от того, нужно это или нет.

Если сервис использует зависимость, он должен проверить сначала ссылки на null, как показано в следующем листинге.

Листинг 4-5: Проверка параметров метода на null перед использование
public string DoStuff(SomeValue value, ISomeContext context)
{
	if (context == null)
	{
		throw new ArgumentNullException("context");
	}
	return context.Name;
}

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

Если метод не использует внедренную зависимость, ему не нужно содержать ограждающее условие. Это звучит странно, ведь если параметр не используется, то зачем он вообще нужен? Тем не менее, вам может потребоваться сохранить его, если метод является частью реализации интерфейса.

Когда это использовать

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

Это часто бывает в сценариях надстройки, где надстройка предоставлена с информацией о контексте времени выполнения через параметр метода. В таких случаях требуется, чтобы надстройка реализовывала интерфейс, который определяет метода (методы) с внедрением.

Представьте интерфейс надстройки с такой структурой:

public interface IAddIn
{
	string DoStuff(SomeValue value, ISomeContext context);
}

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

Листинг 4-6: Пример клиента надстройки
public SomeValue DoStuff(SomeValue value)
{
	if (value == null)
	{
		throw new ArgumentNullException("value");
	}
	var returnValue = new SomeValue();
	returnValue.Message = value.Message;
	foreach (var addIn in this.addIns)
	{
		returnValue.Message =
			addIn.DoStuff(returnValue, this.context);
	}
	return returnValue;
}

Строки 11-12: Передача контекста надстройке

Закрытое поле AddIns является спискам экземпляров IAddIn, что позволяет клиенту пройти циклом по списку для вызова каждого метода надстройки DoStuff. Каждый раз, когда метод DoStuff вызывается для надстройки, контекст операции, представленный полем context, передается в качестве параметра метода.

Примечание

Внедрение в метод тесно связано с использованием фабрик абстракций, описанных в разделе 6.1. Любая фабрика абстракций, которая принимает абстракцию в качестве входных данных, может рассматриваться как вариант внедрения в метод.

Время от времени, значение и контекст операции инкапсулируются в одной абстракции, которая работает как комбинация обоих.

Таблица 4-3: Преимущества и недостатки внедрения в метод
Преимущества Недостатки
Позволяет вызывающему элементу предоставить конкретный для операции контекст Ограниченная применяемость

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

Использование

.NET BCL дает много примеров внедрения в метод, особенно в пространстве имен System.ComponentModel.

System.ComponentModel.Design.IDesigner используется для реализации пользовательского функционала времени разработки для компонентов. Он имеет метод Initialize, который принимает экземпляр IComponent, поэтому он знает, какой компонент он в настоящее время помогает разрабатывать. Дизайнеры создаются реализациями IDesignerHost, которые также принимают экземпляры IComponent в качестве параметров для создания дизайнеров:

IDesigner GetDesigner(IComponent component);

Это хороший пример сценария, когда параметр сам несет в себе информацию: компонент может нести информацию о том, какой IDesigner создать, но в то же время, это также компонент, над которым должен впоследствии работать дизайнер.

Другой пример в пространстве имен System.ComponentModel обеспечивается классом TypeConverter. Некоторые из его методов принимают экземпляр ITypeDescriptorContext, который, как следует из названия, передает информацию о контексте текущей операции. Поскольку таких методов много, я не хочу перечислять их все, но вот характерный пример:

public virtual object ConvertTo(ITypeDescriptorContext context,
	CultureInfo culture, object value, Type destinationType)

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

ASP.NET MVC также содержит несколько примеров внедрения в метод. Интерфейс IModelBinder может быть использован для преобразования HTTP GET или POST данных в строго типизированные объекты. Вот метод:

object BindModel(ControllerContext controllerContext,
	ModelBindingContext bindingContext);

В методе BindModel параметр controllerContext содержит информацию о контексте операции (между прочим, HttpContext), тогда как BindingContext несет в себе больше явной информации о значениях, полученных от браузера.

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

Пример: конвертация валюты в корзине

В предыдущих примерах мы видели, как BasketController в примере коммерческого приложения извлекает предпочтительную валюту пользователя. Я дополню пример конвертации валюты путем конвертирования Basket в валюту пользователя.

Currency – это абстракция, которая моделирует валюту.

Листинг 4-7: Currency
public abstract class Currency
{
	public abstract string Code { get; }
	public abstract decimal GetExchangeRateFor(
		string currencyCode);
}

Свойство Code возвращает код валюты для экземпляра Currency. Ожидается, что коды Currency – это международные коды валют. Например, код валюты для датской кроны – это DKK, в то время как USD – это доллары США.

Метод GetExchangeRateFor возвращает обменный курс между экземпляром Currency и другой валютой. Заметим, что это абстрактный метод и обозначает, что я не делаю никаких предположений о том, как обменный курс будет найден исполнителем.

В следующем разделе мы рассмотрим, как экземпляры Currency используются для преобразования цен, и как эта абстракция может быть реализована и проведена так, чтобы вы могли конвертировать некоторые цены в такие валюты как доллары США или евро.

Внедрение Currency

Вы будете использовать абстракцию Currency как несущую информацию зависимость, чтобы выполнить конверсию валют для корзины, так что вы добавите метод ConvertTo к классу Basket:

public Basket ConvertTo(Currency currency)

Он пройдет циклом по всем элементам в корзине и сконвертирует их подсчитанные цены в приведенную валюту, возвращая новый экземпляр Basket со сконвертированными элементами. Через серию делегированных вызовов метода, реализация, наконец, предоставляется классом Money, как показано в следующем листинге.

Листинг 4-8: Конвертация Money в другую валюту
public Money ConvertTo(Currency currency)
{
	if (currency == null)
	{
		throw new ArgumentNullException("currency");
	}
	var exchangeRate =
		currency.GetExchangeRateFor(this.CurrencyCode);
	return new Money(this.Amount * exchangeRate,
		currency.Code);
}

Строка 1: Внедрить Currency в качестве параметра метода

Currency внедряется в метод ConvertTo через параметр currency и проверяется вездесущим ограждающим условием, которое гарантирует, что экземпляр currency доступен для остальной части тела метода.

Обменный курс к текущей валюте (представленный this.CurrencyCode) извлекается из предоставленной currency, используется для расчета и возвращает новый экземпляр Money.

С методами ConvertTo вы можете, наконец, реализовать метод Index для BasketController, как показано в следующем листинге.

Листинг 4-9: Конвертация валюты в Basket
public ViewResult Index()
{
	var currencyCode =
		this.CurrencyProfileService.GetCurrencyCode();
	var currency =
		this.currencyProvider.GetCurrency(currencyCode);
	var basket = this.basketService
		.GetBasketFor(this.User)
		.ConvertTo(currency);
	if (basket.Contents.Count == 0)
	{
		return this.View("Empty");
	}
	var vm = new BasketViewModel(basket);
	return this.View(vm);
}

Строки 7-9: Конвертация пользовательской корзины в выбранную валюту

BasketController использует экземпляр IBasketService для получения пользовательской корзины. Вы можете вспомнить из главы 2, что зависимость IBasketService предоставляется BasketController через внедрение в конструктор. Как только у вас есть экземпляр Basket, вы можете конвертировать его в нужную валюту, используя метод ConvertTo, переданный экземпляру currency.

В данном случае вы используете внедрение в метод, потому что абстракция Currency несет информацию, но будет варьировать по контексту (в зависимости от выбора пользователя). Вы могли бы реализовать тип Currency в качестве конкретного класса, но это ограничило бы вашу способность определять, как извлекаются валютные курсы.

Теперь, когда мы увидели, как используется класс Currency, пришло время изменить нашу точку зрения и посмотреть, как он может быть реализован.

Реализация Currency

Я еще не говорил о том, как реализован класс Currency, потому что это не столь важно с точки зрения внедрения в метод. Как вы помните из раздела 4.1.4 и как вы можете видеть в листинге 4-9, экземпляр Currency обрабатывается экземпляром CurrencyProvider, который мы внедрили в класс BasketController путем внедрения в конструктор.

Чтобы упростить пример, я показал, что произойдет, если вы решили реализовать CurrencyProvider и Currency при помощи базы данных SQL Server и LINQ to Entities. Это предполагает, что в базе данных имеется таблица с курсами валют, которая была заполнена заранее каким-то внешним механизмом. Вы также могли бы использовать веб-сервис, чтобы запросить обменные курсы из внешнего источника.

Реализация CurrencyProvider передает строку подключения для реализации Currency, которая использует эту информацию для создания ObjectContext. Суть дела заключается в реализации метода GetExchangeRateFor, показанного в следующем листинге.

Листинг 4-10: Реализация Currency, поддерживаемая SQL Server
public override decimal GetExchangeRateFor(string currencyCode)
{
	var rates = (from r in this.context.ExchangeRates
		where r.CurrencyCode == currencyCode
		|| r.CurrencyCode == this.code
		select r)
		.ToDictionary(r => r.CurrencyCode);
	return rates[currencyCode].Rate / rates[this.code].Rate;
}

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

Эта реализация потенциально выполняет много коммуникаций «вне процесса» с базой данных. Метод ConvertTo в Basket в конечном итоге вызывает этот метод в цикле, и обращение к базе данных при каждом вызове, скорее всего, будет иметь пагубные последствия для производительности. Я вернусь к этой проблеме в следующем разделе.

Связанные паттерны

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

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