Главная страница

Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей


Скачать 5.66 Mb.
НазваниеРуководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
АнкорВнедрение зависимостей в .net
Дата14.12.2019
Размер5.66 Mb.
Формат файлаpdf
Имя файлаВнедрение зависимостей в .NET.pdf
ТипРуководство
#100226
страница10 из 43
1   ...   6   7   8   9   10   11   12   13   ...   43
Внедрение в метод лучше использовать тогда, когда зависимость может меняться с каждым вызовом метода. Это может быть в том случае, когда зависимость сама по себе представляет значение, но когда вызывающий элемент желает предоставить потребителю информацию о контексте, в котором вызывается операция.
Это часто бывает в сценариях надстройки, где надстройка предоставлена с информацией о контексте времени выполнения через параметр метода. В таких случаях требуется, чтобы надстройка реализовывала интерфейс, который определяет метода (методы) с внедрением.
Представьте интерфейс надстройки с такой структурой: public interface IAddIn
{ string DoStuff(SomeValue value, ISomeContext context);
}
Любой класс, реализующий этот интерфейс, может быть использован в качестве надстройки. Некоторые классы могут вообще не заботиться о контексте, в то время как другие реализации должны. Клиент может использовать список надстроек, вызывая каждую со значением и контекстом, возвращая суммарный результат. Это показано в следующем листинге.
Листинг 4-6: Пример клиента надстройки
1.
public SomeValue DoStuff(SomeValue value)
2.
{
3.
if (value == null)
4.
{
5.
throw new ArgumentNullException("value");
6.
}
7.
var returnValue = new SomeValue();
8.
returnValue.Message = value.Message;
9.
foreach (var addIn in this.addIns)
10.
{
11.
returnValue.Message =
12.
addIn.DoStuff(returnValue, this.context);
13.
}
14.
return returnValue;
15.
}
Строки 11-12: Передача контекста надстройке
Закрытое поле
AddIns является спискам экземпляров
IAddIn
, что позволяет клиенту пройти циклом по списку для вызова каждого метода надстройки
DoStuff
. Каждый раз,

139 когда метод
DoStuff вызывается для надстройки, контекст операции, представленный полем context
, передается в качестве параметра метода.
П римечание
Внедрение в метод тесно связано с использованием фабрик абст ракций, описанных в разделе 6.1. Любая фабрика абстракций, которая принимает абстракцию в качестве входных данных, может рассматриваться как вариант внедрения в метод.
Время от времени, значение и контекст операции инкапсулиру ются в одной абстракции, которая работает как комбинация обоих.
Таблица 4-3: Преимущества и недостатки внедрения в метод
Преимущества
Недостатки
Позволяет вызывающему элементу предоставить конкретный для операции контекст
Ограниченная применяемость
Внедрение в метод отличается от других DI паттернов, которое мы видели до сих пор, тем, что внедрение не происходит в Com position 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
, в то время как значение для преобразования и конечный тип передаются в виде отдельных параметров.

140
Исполнители могут использовать или игнорировать параметр 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 используются для преобразования цен, и как эта абстракция может быть реализована и проведена так, чтобы вы могли конвертировать некоторые цены в такие валюты как доллары США или евро.

141
Внедрение Currency
Вы будете использовать абстракцию
Currency как несущую информацию зависимость, чтобы выполнить конверсию валют для корзины, так что вы добавите метод
ConvertTo к классу
Basket
: public Basket ConvertTo(Currency currency)
Он пройдет циклом по всем элементам в корзине и сконвертирует их подсчитанные цены в приведенную валюту, возвращая новый экземпляр
Basket со сконвертированными элементами. Через серию делегированных вызовов метода, реализация, наконец, предоставляется классом
Money
, как показано в следующем листинге.
Листинг 4-8: Конвертация
Money в другую валюту
1.
public Money ConvertTo(Currency currency)
2.
{
3.
if (currency == null)
4.
{
5.
throw new ArgumentNullException("currency");
6.
}
7.
var exchangeRate =
8.
currency.GetExchangeRateFor(this.CurrencyCode);
9.
return new Money(this.Amount * exchangeRate,
10.
currency.Code);
11.
}
Строка 1: Внедрить
Currency в качестве параметра метода
Currency внедряется в метод
ConvertTo через параметр currency и проверяется вездесущим ограждающим условием, которое гарантирует, что экземпляр currency доступен для остальной части тела метода.
Обменный курс к текущей валюте (представленный this.CurrencyCode
) извлекается из предоставленной currency
, используется для расчета и возвращает новый экземпляр
Money
С методами
ConvertTo вы можете, наконец, реализовать метод
Index для
BasketController
, как показано в следующем листинге.
Листинг 4-9: Конвертация валюты в
Basket
1.
public ViewResult Index()
2.
{
3.
var currencyCode =
4.
this.CurrencyProfileService.GetCurrencyCode();
5.
var currency =
6.
this.currencyProvider.GetCurrency(currencyCode);
7.
var basket = this.basketService
8.
.GetBasketFor(this.User)
9.
.ConvertTo(currency);
10.
if (basket.Contents.Count == 0)
11.
{
12.
return this.View("Empty");
13.
}
14.
var vm = new BasketViewModel(basket);
15.
return this.View(vm);
16.
}

142
Строки 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 Ent ities.
Это предполагает, что в базе данных имеется таблица с курсами валют, которая была заполнена заранее каким-то внешним механизмом. Вы также могли бы использовать веб- сервис, чтобы запросить обменные курсы из внешнего источника.
Реализация
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 в конечном итоге вызывает этот метод в цикле, и

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

144 4.4. Окружающий контекст (Ambient
Context)
Как мы можем сделат ь зависимост ь доступной для каждого модуля, не загряз няя
каждый API Cross-Cutting Concerns?
Делая ее пригодной для использования через статический доступ.
Рисунок 4-8: Каждый модуль при надобности может получить доступ к Ambient Context
По-настоящему универсальный Cross-Cutting Concern потенциально может загрязнить большую часть API для приложения, если вам нужно передать экземпляр каждому элементу. В качестве альтернативы можно определить контекст, доступный всем, кто в нем нуждается, и который может быть проигнорирован всеми остальными.
Как это работает
Am bient Context (окружающий контекст) доступен любому потребителю через статическое свойство или метод. Потребляющий класс может использовать его так: public string GetMessage()
{ return SomeContext.Current.SomeValue;
}
В данном случае контекст имеет статическое свойство
Current
, к которому потребитель может получить доступ. Это свойство может быть по-настоящему статическим или может быть связано с выполняемым в данный момент потоком.
Чтобы быть полезным в DI сценариях, контекст сам по себе должен быть абстракцией и должна иметься возможность менять контекст извне: для предыдущего примера это обозначает, что свойство
Current должно быть доступно для записи. Контекст сам по себе может быть реализован так, как показано в следующем листинге.

145
Листинг 4-11: Ambient Context
1.
public abstract class SomeContext
2.
{
3.
public static SomeContext Current
4.
{
5.
get
6.
{
7.
var ctx =
8.
Thread.GetData(
9.
Thread.GetNamedDataSlot("SomeContext"))
10.
as SomeContext;
11.
if (ctx == null)
12.
{
13.
ctx = SomeContext.Default;
14.
Thread.SetData(
15.
Thread.GetNamedDataSlot("SomeContext"),
16.
ctx);
17.
}
18.
return ctx;
19.
}
20.
set
21.
{
22.
Thread.SetData(
23.
Thread.GetNamedDataSlot("SomeContext"),
24.
value);
25.
}
26.
}
27.
public static SomeContext Default =
28.
new DefaultContext();
29.
public abstract string SomeValue { get; }
30.
}
Строки 7-10: Получить текущий контекст из T LS
Строки 22-24: Сохранить текущий контекст в T LS
Строка 29: Значение, переносимое контекстом
Контекст является абстрактным классом, который позволяет заменить один контекст другой реализацией во время выполнения.
В данном примере свойство
Current сохраняет текущий контекст в Локальном
Хранилище Потока (Thread Local Storage (TLS)), что обозначает, что каждый поток имеет свой собственный контекст, который независим от контекста любого другого потока. В случаях, когда для TLS не был присвоен контекст, возвращается реализация по умолчанию. Важно иметь возможность гарантировать, что ни один потребитель не получит
NullReferenceException
, когда он попытается получить доступ к свойству
Current
, поэтому нужно иметь хорошую Local Default. Отметим, что в этом случае свойство
Default распределяется по всем потокам. Это работает, потому что в данном примере
DefaultContext
(класс, который наследуется от
SomeContext
) является неизменным. Если контекст по умолчанию изменяемый, вам нужно будет назначить отдельный экземпляр для каждого потока, чтобы предотвратить перекрестное загрязнение потоков.
Внешние клиенты могут назначить новый контекст для T LS. Обратите внимание, что возможно присвоить null
, но если это произойдет, то следующая операция чтения автоматически переназначит контекст по умолчанию.

146
Весь смысл использования Am bient Context заключает ся во взаимодейст вии с ним. В данном примере это взаимодействие представлено одиночным абстрактным строковым свойством, но контекстный класс может быть и простым, и сложным, когда это необходимо.
Внимание
Для простоты я слегка пропустил безопасность потоков в коде в листинге 4-11. Если вы решили реализовать основанный на TLS Am bient Context, убедитесь, что вы знаете, что делаете.
С овет
Пример в листинге 4-11 использует T LS, но вы также можете использовать
CallContext для получения подобного результата.
П римечание
Ambient Context не обязательно должен быть связан с потоком или вызываемым контекстом. Иногда имеет больше смысла сделать так, чтобы он применялся ко всему
AppDom ain, указав его как static
Если вы хотите заменить контекст по умолчанию пользовательским контекстом, вы можете создать пользовательскую реализацию, которая наследуется от контекста, и назначить ее в нужное время:
SomeContext.Current = new MyContext();
Для контекста на основе T LS вы должны присвоить пользовательский экземпляр, когда вы создаете новый поток, в то время как по-настоящему универсальный контекст можно назначить в Com position Root.
Когда это использовать
Ambient Context должен быть использован только в редчайших случаях. Для большинства случаев больше подходят внедрение в конструктор или внедрение в свойство, но у вас может быть реальный Cross-Cutting Concern, который загрязняет каждый API в вашем приложении, если вам нужно передать его всем сервисам.
Внимание
Ambient Context сходный по структуре с анти-паттерном Service Locator, который я опишу в главе 5. Разница состоит в том, что Am bient Cont ext предоставляет экземпляр только одной, строго типизированной зависимости, в то время как Service Locator предположительно должен обеспечить экземпляры для каждой зависимости, которую вы можете запросить. Различия являются тонкими, так что убедитесь, что вы полностью понимаете, когда следует применять Am bient Cont ext, прежде чем сделать это. Если вы сомневаетесь, выберите другой DI паттерн.
В разделе 4.4.4, что я реализую
TimeProvider
, который может быть использован, чтобы получить текущее время, и я также объясню, почему я предпочитаю его статическим членам
DateTime
. Т екущее время является настоящим Cross-Cutting Concern, потому что

147 вы не можете предсказать, каким классам в каких слоях оно может понадобиться.
Большинство классов, вероятно, могут использовать текущее время, но лишь небольшая часть из них собираются это сделать.
Потенциально это может заставить вас писать много кода с дополнительным параметром
TimeProvider
, потому что вы не знаете, когда он сможет вам понадобиться: public string GetSomething(SomeService service,
TimeProvider timeProvider)
{ return service.GetStuff("Foo", timeProvider);
}
Предыду щий метод передает параметр
TimeProvider для этого сервиса. Это может выглядеть безобидно, но когда мы затем просмотрим метод
GetStuff
, мы обнаружим, что это никогда не используется: public string GetStuff(string s, TimeProvider timeProvider)
{ return this.Stuff(s);
}
В данном случае параметр
TimeProvider передается в качестве дополнительного багажа только потому, что он может однажды понадобиться. Это загрязняет API ненужными
CCC, и код становится дурнопахнущим.
Ambient Context может быть решением этой проблемы, если встретятся условия, описанные в таблице 4-4.
Таблица 4-4: Условия для реализации Ambient Context
Условие
Описание
Вам нужно, чтобы контекст был запрашиваемым.
Если вам нужно только записать некоторые данные (все методы для контекста возвращают
1   ...   6   7   8   9   10   11   12   13   ...   43


написать администратору сайта