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

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


Скачать 5.66 Mb.
НазваниеРуководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
АнкорВнедрение зависимостей в .net
Дата14.12.2019
Размер5.66 Mb.
Формат файлаpdf
Имя файлаВнедрение зависимостей в .NET.pdf
ТипРуководство
#100226
страница9 из 43
1   ...   5   6   7   8   9   10   11   12   ...   43
Внедрение в конструкт ор является наиболее применимым DI паттерном, а также его легче всего реализовать правильно. Он применяется, когда зависимость является
обязательной.
Если нам нужно сделать зависимость опциональной, мы можем перейти ко внедрению в
свойст во (Property Injection), если у нас есть подходящая Local Default.
Когда зависимость представляет Cross-Cutting Concern (CCC), который должен быть потенциально доступен для любого модуля в приложении, мы можем использовать
Ambient Context.
Следующим паттерном в этой главе является внедрение в свойство, которое тесно связано с внедрением в конструктор; единственный решающий параметр заключается в том, является ли зависимость обязательной или нет.

130 4.2. Внедрение в свойство (Property
Injection)
Как мы можем включит ь DI в качестве опции в классе, когда у нас есть хорошая Local
Default?
Раскрывая записываемое свойство, которое позволяет вызывающим элементам предоставить зависимость, если они хотят переопределить поведе ние по умолчанию.
Рисунок 4-5:
SomeClass имеет опциональную зависимость для
ISomeInterface
; вместо того, чтобы требовать от вызывающих элементов предоставить экземпляр, он дает вызывающи м элементам возможность определить его через свойство.
Когда класс имеет хорошую Local Default, но мы все еще хотим оставить его открытой для расширения, мы можем раскрыть доступное для записи свойство, что позволяет клиенту указать другую реализацию зависимости класса, чем по умолчанию.
Примечание
Внедрение в свойст во также известно как внедрение в сетт ер.
В соответствии с рисунком 4-5, клиенты, желающие воспользоваться
SomeClass как есть, могут создать новый экземпляр класса и использовать его, в то время как клиенты, желающие изменить поведение класса могут это сделать путем установки свойства
Dependency для другой реализации
ISomeInterface
Как это работает
Класс, который использует зависимость, должен предоставить открытое, доступное для записи свойство типа зависимости. В каркасной реализации это может быть таким же простым, как следующий листинг.
Листинг 4-3: Внедрение в свойство public partial class SomeClass
{ public ISomeInterface Dependency { get; set; }
}
SomeClass зависит от
ISomeInterface
. Клиенты могут поставлять реализации
ISomeInterface
, устанавливая свойство
Dependency
. Обратите внимание, что в отличие от внедрения в конструктор, вы не можете отметить поле свойства
Dependency как readonly
, потому что вы разрешаете вызывающим элементам менять это свойство в любой момент жизненного цикла
SomeClass

131
Другие члены класса могут использовать внедренную зависимость, чтобы выполнять свои обязанности, например: public string DoSomething(string message)
{ return this.Dependency.DoStuff(message);
}
Однако такая реализация является хрупкой, потому что нет гарантии, что свойство
Dependency возвращает экземпляр
ISomeInterface
. Код, как этот, выбросит
NullReferenceException
, так как значение свойства
Dependency равно null
: var mc = new SomeClass(); mc.DoSomething("Ploeh");
Эта проблема может быть решена, если позволить конструктору установить экземпляр по умолчанию для свойства в сочетании с ограждающим условием в сеттере свойства.
Другая проблема возникает, если вы позволяете клиентам переключать зависимость в середине времени жизни класса. Эту проблему можно решить путем введения внутреннего флага, который позволяет клиенту установить зависимость один раз.
Пример далее показывает, как можно справиться с этими сложностями, но прежде, чем я доберусь до этого, я хотел бы объяснить, когда уместно пользоваться внедрением в свойство.
Когда это использовать
Внедрение в свойство следует использовать только тогда, когда класс, который вы разрабатываете, имеет хорошую Local Default, и вы все еще хотите разрешить вызывающим элементам предоставлять различные реализации зависимости класса.
Внедрение в свойство лучше использовать, когда зависимость не являет ся обя зат ельной.
П римечание
Существует полемика вокруг вопроса о том, показывает ли внедрение в свойство необязательную зависимость. В качестве общего принципа API дизайна я считаю, что свойства необязательны, потому что вы можете легко забыть назначить их, и компилятор не жалуется. Если вы согласны с этим принципом в общем, вы также должны принять его в частном случае с DI.
Local Defaul t
Когда вы разрабатываете класс, у которого есть зависимость, вы, возможно, думаете о конкретной реализации этой зависимости. Если вы пишете доменный сервис, который обращается к хранилищу, вы, скорее всего, планируете разработать реализацию этого хранилища, которая использует реляционную базу данных.
Заманчиво было бы сделать так, чтобы эта реализация использовалась по умолчанию классом на стадии разработки. Однако когда такой предполагаемый элемент по умолчанию реализован в другой сборке, использование его как дефолтного означало бы

132 создание жесткой ссылки на эту другую сборку, что нарушает многие преимущества слабой связанности, описанной в главе 1.
И наоборот, если предполагаемая реализация по умолчанию определяется в той же библиотеке как класс, у вас не будет этой проблемы. К сожалению, это не случай с хранилищами, но такие Local Default чаще всего встречаются как стратегии.
Пример в этом разделе содержит Local Default.
Рисунок 4-6: Даже в пределах одного модуля мы можем ввести абстракции
(представлены вертикальным прямоугольником), которые помогают снизить связанность классов в этом модуле. Основным мотивом для этого является повышение поддержки модуля, что достигается тогда, когда классы варьируют независимо друг от друга.
В главе 1 мы обсуждали много веских причин для написания кода со слабой связанностью, когда модули изолированы друг от друга. Тем не менее, слабая связанность может с большим успехом также применяться к классам в одном модуле. Часто это делается путем введения абстракции в пределах одного модуля, когда классы «общаются» через абстракции, вместо того чтобы быть тесно связанными друг с другом.
Рисунок 4-6 иллюстрирует, что абстракции могут быть определены, реализованы и использованы внутри одного модуля с основной целью открытия классов для расширения.
Примечание
Концепция открытия класса для расширения охватывается принципом открытости/закрытости (Open/Closed Principle), который, если вкратце, утверждает, что класс должен быть открытым для расширения, но закрытым для изменений.
Когда мы реализуем классы, следуя принципу открытости/закр ыто сти, мы можем иметь в виду Local Default, но мы по-прежнему даем клиентам способ расширить класс, заменив зависимость чем-то еще.
Примечание
Внедрение в свойство является лишь одним из многих способов применения принципа открытости/закрытости.

133
С овет
Иногда вы только хотите дать точку расширения, но оставить Local Default как пустую операцию. В таких случаях вы можете использовать паттерн Null Object для реализации
Local Default.
С овет
Иногда вы хотите оставить Local Default на месте, но иметь возможность добавить больше реализаций. Вы можете добиться этого путем моделирования зависимости вокруг паттернов Наблюдатель (Observer) или Компоновщик (Com posite).
До сих пор я не показал вам ни одного примера внедрения в свойство, потому что применимость этого паттерна является более ограниченной.
Таблица 4-2: Преимущества и недостатки внедрения в свойство
Преимущества Недостатки
Легко понять Ограниченная применимость
Не совсем просто реализовать надежно
Основным преимуществом внедрения в свойство является то, что его легко понять. Я часто видел, как этот паттерн используется в качестве первой попытки, когда люди решают применять DI.
Представление может быть обманчивым, и внедрение в свойство сопряжено с трудностями. Его сложно реализовать надежным образом. Клиенты могут забыть (или не захотеть) предоставить зависимость, или по ошибке присвоить null в качестве значения.
Кроме того: что должно произойти, если клиент попытается изменит ь зависимость в середине жизненного цикла класса? Это может привести к противоречивому или неожиданному поведению, поэтому вы можете захотеть защитить себя от этого события.
С внедрением в конструктор вы можете защитить класс против таких инцидентов, применяя ключевое слово readonly к полю, но это невозможно, когда вы раскрываете зависимость как записываемое свойство. Во многих случаях внедрение в конструктор гораздо проще и более надежно, но бывают ситуации, когда внедрение в свойство является правильным выбором. Это в том случае, когда предоставление зависимости является необязательным, потому что у вас есть хорошая Local Default.
Существование хорошей Local Default частично зависит от степени детализации модулей.
.NET Base Class Library (BCL) поставляется как довольно большой пакет; до тех пор, пока
default остается в пределах BCL, можно утверждать, что она также и local. В следующем разделе я кратко остановлюсь на этой теме.
Использование
В .NET BCL, внедрение в свойство является немного более используемым, чем внедрение в конструктор, вероятно, потому что много хороших Local Default определяются в разных местах.

134
System.ComponentModel.IComponent имеет доступное для записи свойство
Site
, которое позволяет определить экземпляр
ISite
. Это главным образом используется в разработке сценариев (например, Visual Studio), чтобы изменить или усилить компонент, когда он находится в дизайнере.
Другой пример, который сильнее отражает то, как мы привыкли думать о DI, можно найти в Windows Workflow Foundation. Класс
WorkflowRuntime дает вам возможность добавлять, получать и удалять сервисы. Это не совсем внедрение в свойство, потому что API позволяет добавлять ноль или несколько нетипизированных сервисов посредством одного
API общего назначения: public void AddService(object service) public T GetService() public object GetService(Type serviceType) public void RemoveService(object service)
Хотя
AddService выбросит
ArgumentNullException если значение сервиса является null
, нет никакой гарантии, что вы можете получить сервис заданного типа, потому что он, возможно, никогда не будет добавлен к текущему экземпляру
WorkflowRuntime
(на самом деле, это потому что метод
GetService является Service Locator).
С другой стороны,
WorkflowRuntime поставляется с большим количеством Local Default для каждого из требуемых сервисов, которые ему нужны, и они даже имеют префикс
Default, например
DefaultWorkflowSchedulerService и
DefaultWorkflowLoaderService
Если, например, не добавлен альтернативный
WorkflowSchedulerService либо с помощью метода
AddService
, либо конфигурационного файла приложения, используется класс
DefaultWorkflowSchedulerService
После этих BCL примеров давайте перейдем к более существенным примерам использования и реализации внедрения в свойство.
Пример: Определение сервиса профиля валюты для B asketController
В разделе 4.1.4 я начал добавлять функционал по конверсии валюты в пример коммерческого приложения и вкратце показал вам некоторую реализацию метода
Index в
BasketController
, но умолчал о появлении
CurrencyProfileService
. Дело вот в чем:
Приложению нужно знать, какую валюту пользователь желает видеть. Если обратиться к рисунку 4-4, вы заметите некоторые ссылки на валюту в нижней части экрана. Когда пользователь нажимает одну из этих ссылок, вы должны сохранить где-то выбранную валюту и связать этот выбор с пользователем.
CurrencyProfileService облегчает хранение и загрузку выбранной пользователем валюты: public abstract class CurrencyProfileService
{ public abstract string GetCurrencyCode(); public abstract void UpdateCurrencyCode(string currencyCode);
}
Это абстракция, которая кодирует действия применения и извлечения текущего пользовательского выбора валюты.

135
В ASP.NET MVC (и ASP.NET в целом) у вас есть известная часть инфраструктуры, которая занимается таким сценарием: сервис
Profile
. Отличная реализация Local Default для
CurrencyProfileService это то, что оборачивает ASP.NET сервис
Profile и обеспечивает необходимую функциональнос ть, определенную методами
GetCurrencyCode и
UpdateCurrencyCode
BasketController будет использовать этот
DefaultCurrencyProfileService по умолчанию, когда раскрывает свойство, которое позволит вызывающему элементу заменить его чем-то другим.
Листинг 4-4: Раскрытие свойства
CurrencyProfileService
1.
private CurrencyProfileService currencyProfileService;
2.
public CurrencyProfileService CurrencyProfileService
3.
{
4.
get
5.
{
6.
if (this.currencyProfileService == null)
7.
{
8.
this.CurrencyProfileService =
9.
new DefaultCurrencyProfileService(
10.
this.HttpContext);
11.
}
12.
return this.currencyProfileService;
13.
}
14.
set
15.
{
16.
if (value == null)
17.
{
18.
throw new ArgumentNullException("value");
19.
}
20.
if (this.currencyProfileService != null)
21.
{
22.
throw new InvalidOperationException();
23.
}
24.
this.currencyProfileService = value;
25.
}
26.
}
Строки 6-12: Отложенная инициализация Local Default
Строки 20-23: Зависимость определяется только один раз
DefaultCurrencyProfileService сам использует внедрение в конструктор, потому что ему нужен доступ к
HttpContext и потому что
HttpContext не доступен для
BasketController во время создания; он должен отложить создание
DefaultCurrencyProfileService
, пока свойство не будет запрошено впервые. В этом случае требуется отложенная инициализация, но в других случаях Local Default может быть назначена в конструкторе. Обратите внимание, что Local Default назначается через открытый сеттер, который гарантирует, что все ограничивающие условия были оценены.
Первое ограждающее условие гарантирует, что зависимость не имеет значение null
Следующее ограждающее условие гарантирует, что зависимость может быть назначена только один раз. В данном случае я предпочитаю, чтобы
CurrencyProfileService не мог быть изменен после того, как был назначен, поскольку в противном случае это может привести к противоречивому поведению, где выбор валюты сначала сохраняется при помощи одного
CurrencyProfileService
, а затем извлекается из другого места, что, скорее всего, дает другое значение.

136
Вы можете также заметить, что поскольку вы используете сеттер для отложенной инициализации, зависимость будет также заблокирована, как только свойство будет прочтено. Еще раз, это является защитой клиентов от случая, когда зависимость впоследствии меняется без уведомления.
Если вы прошли через все ограждающие условия, вы можете сохранить экземпляр для дальнейшего использования.
По сравнению с внедрением в конструктор, это гораздо более сложно. Внедрение в свойство может выглядеть простым в сыром виде, как показано в листинге 4-3, но при правильном применении это, как правило, гораздо более сложно: и в этом примере я даже проигнорировал проблему безопасности потока.
Когда
CurrencyProfileService на месте, метод
Index в
BasketController теперь может использовать его для получения предпочтительной валютой пользователя: public ViewResult Index()
{ var currencyCode = this.CurrencyProfileService.GetCurrencyCode(); var currency = this.currencyProvider.GetCurrency(currencyCode);
// …
}
Это тот же фрагмент кода, что показан в разделе 4.1.4.
CurrencyProfileService используется для получения выбранной пользователем валюты, а
CurrencyProvider в дальнейшем используется для извлечения этой валюты.
В разделе 4.3.4, я вернусь к методу
Index
, чтобы показать, что произойдет дальше.
Связанные паттерны
Вы используете внедрение в свойст во, когда зависимость не является обязательной, потому что у вас есть хорошая Local Default. Если у вас нет Local Default, вы должны изменить реализацию на внедрение в конст руктор.
Когда зависимость представляет CROSS- CUTTING CONCERN, который должен быть доступен для всех модулей в приложении, вы можете реализовать его как Ambient Context.
Но прежде чем мы перейдем к этому, внедрение в мет од, описанное в следующем разделе, требует несколько иного подхода, поскольку его чаще применяют в ситуациях, когда у нас уже есть зависимость, которую мы хотим передать отдельным операциям.

137 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;
}

138
Ограждающее условие гарантирует, что контекст доступен для остальной части тела метода. В данном примере метод использует имя контекста для возвращения значения, поэтому важно убедиться, что контекст доступен.
Если метод не использует внедренную зависимость, ему не нужно содержать ограждающее условие. Это звучит странно, ведь если параметр не используется, то зачем он вообще нужен? Тем не менее, вам может потребоваться сохранить его, если метод является частью реализации интерфейса.
Когда это использовать
1   ...   5   6   7   8   9   10   11   12   ...   43


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