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

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


Скачать 5.66 Mb.
НазваниеРуководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
АнкорВнедрение зависимостей в .net
Дата14.12.2019
Размер5.66 Mb.
Формат файлаpdf
Имя файлаВнедрение зависимостей в .NET.pdf
ТипРуководство
#100226
страница37 из 43
1   ...   33   34   35   36   37   38   39   40   ...   43
расширения контейнера (Cont ainer Extensions).
Целью расширения контейнера Unity является не только пакетирование конфигурации в повторно используемые пакеты. Как следует из его названия, расширение контейнера также можно использовать для того, чтобы расширять функциональность контейнера

543
Unity. Например, такая возможность контейнера Unity, как механизм перехвата, реализуется в виде расширения контейнера (это конкретное расширение мы рассмотрим в разделе 14.3.4).
Несмотря на то, что расширения контейнера можно использовать для множества различных целей, их также можно использовать и для разбиения конфигурации на модули. Все, что нам нужно для реализации расширения контейнера, – выполнить наследование от абстрактного класса
UnityContainerExtension и реализовать метод
Initialize этого класса. В следующем листинге продемонстрировано, как можно с легкостью преобразовать код из листинга 14-1 в расширение контейнера.
Листинг 14-2: Реализация расширения контейнера public class IngredientExtension : UnityContainerExtension
{ protected override void Initialize()
{ var a = typeof(Steak).Assembly; foreach (var t in a.GetExportedTypes())
{ if (typeof(IIngredient).IsAssignableFrom(t))
{ this.Container.RegisterType( typeof(IIngredient), t, t.FullName);
}
}
}
}
Класс
IngredientExtension наследуется от абстрактного класса
UnityContainerExtension для того, чтобы пакетировать основанную на соглашениях конфигурацию из листинга 14-1 в повторно используемый класс. При наследовании от класса
UnityContainerExtension необходимо реализовать абстрактный метод
Initialize
, в котором вы можете выполнить все необходимые вам действия.
Единственное функциональное отличие от листинга 14-1 заключается в том, что теперь вместо локальной переменной вы вызываете метод
RegisterType унаследованного свойства
Container
Чтобы воспользоваться расширением контейнера, можно вызвать метод
AddExtension или соответствующий метод расширения. Если расширение обладает конструктором по умолчанию, то вы можете использовать характерное условное обозначение метода расширения: container.AddNewExtension();
Метод
AddNewExtension вызывает метод
AddExtension
, который вы также можете использовать в тех ситуациях, когда вам необходимо создать модуль вручную: container.AddExtension(new IngredientExtension());
Эти примеры функционально эквивалентны.

544
П одсказка
Расширения контейнера Unity позволяют пакетировать и структурировать конфигурационный код вашего контейнера. Даже если расширения разрабатываются совсем не для этих целей, у вас может появиться желание использовать их вместо однострочной конфигурации. Такой подход сделает вашу Composition Root более читабельной.
Благодаря функциональности расширений контейнера для конфигурирования контейнера
Unity можно использовать как технологию конфигурирования в коде, так и XML- конфигурацию, и даже реализованный пользователем механизм автоматической регистрации (хотя этот подход занимает больше времени). После того как контейнер сконфигурирован, можно приступить к разрешению сервисов с помощью этого контейнера, что описано в разделе 14.1.1 "Разрешение объектов".
Этот раздел познакомил нас с DI-контейнером Unit y и продемонстрировал фундаментальные принципы: как сконфигурировать и впоследствии использовать созданный контейнер для разрешения сервисов. Разрешение сервисов с легкостью выполняется посредством единичного вызова метода
Resolve
, поэтому вся сложность заключается в конфигурировании контейнера. Конфигурировать контейнер можно несколькими способами, включая императивный код и XML. До настоящего момента мы рассматривали только самое основное API, а более продвинутые вопросы в этом разделе еще не охватывались. Одна из самых важных проблем – это управление жизненным циклом компонентов.

545 14.2. Управление жизненным циклом
В главе 8 обсуждался процесс управления жизненным циклом, в том числе наиболее универсальные стили существования, к примеру, Singleton и Transient. Unity поддерживает множество различных стилей существования и позволяет конфигурировать жизненные циклы всех сервисов. Продемонстрированные в таблице 14-2 стили существования являются частью API контейнера Unity.
В контейнере Unity реализации стилей существования Transient, Per Graph и Singleton эквивалентны основным стилям существования, описанным в главе 8. Поэтому я не буду тратить время на рассмотрение этих стилей существования.
П редупреждение
Несмотря на то, что стиль существования Per Resolve совпадает с описанием, приведенным в разделе 8.3.3 "Per Graph", он имеет некоторые известные дефекты, которые делают менее предпочтительным для использования.
Таблица 14-2: Стили существования Unity
Название
Комментарии
T ransient
Этот стиль существования используется по умолчанию. Экземпляр ы контейнером не отслеживаются.
Container
Controlled
В Unity это название используется для обозначения стиля Singleton.
Per Resolve
В Unity это название используется для обозначения стиля Per Graph.
Экземпляры контейнером не отслеживаются.
Hierarchical
Связывает жизненные циклы компонентов с дочерним контейнером (см. раздел 14.2.1).
Per T hread
Для одного потока создается один экземпляр. Экземпляр ы контейнером не отслеживаются.
Externally
Controlled
Разновидность стиля существования Singleton, при котором сам контейнер содержит только хрупкую ссылку на экземпляр, позволяющу ю уничтожать его сборщиком мусора в случае неиспользования.
П одсказка
Используемый по умолчанию стиль существования T ransient является самым безопасным вариантом, но не всегда самым эффективным. Наиболее подходящий стиль существования для потоко-безопасных сервисов – стиль Singleton, но при этом нужно не забывать явным образом регистрировать эти сервисы.
В этом разделе вы познакомитесь со способами определения стилей существования для компонентов – как с помощью кода, так и посредством XML. Кроме того, чтобы продемонстрироват ь, что мы не ограничены встроенными стилями существования контейнера Unit y, мы рассмотрим процесс реализации пользовательского стиля существования. После прочтения этого раздела вы уже сможете использовать стили существования контейнера Unity в своем собственном приложении.

546
Перед тем как приступить к разработке пользовательского стиля существования, необходимо рассмотреть способы конфигурирования и использования стилей существования.
Конфигурирование стиля существования
В этом разделе мы рассмотрим способы управления стилями существования компонентов, которые применяются в контейнере Unit y. Жизненный цикл компонента конфигурируется в рамках регистрации компонентов и может задаваться как в коде, так и в XML. М ы поочередно рассмотрим каждый из этих способов.
Конфигурирова ние стиля существования с помощью кода
Стиль существования конфигурируется с помощью перегрузки метода
RegisterType
, которая используется для регистрации компонентов в целом. По своей простоте она равносильна следующему коду: container.RegisterType( new ContainerControlledLifetimeManager());
В этом примере конкретный класс
SauceBéarnaise конфигурируется в виде Singleton таким образом, что всякий раз при запрашивании
SauceBéarnaise возвращается один и тот же экземпляр. Если нам необходимо преобразовать абстракцию в конкретный класс, имеющий конкретный стиль существования, можно воспользоваться еще одной похожей перегрузкой метода
RegisterType
: container.RegisterType( new ContainerControlledLifetimeManager());
В этом примере
IIngredient преобразуется в
SauceBéarnaise и конфигурируется в виде
Singleton. В двух предыдущих примерах вы использовали перегрузки метода
RegisterType
, которые в качестве аргумента принимали экземпляр
LifetimeManager
Вместо
ContainerControlledLifetimeManager вы можете использовать любой другой класс, унаследованный от абстрактного класса
LifetimeManager
. В контейнере Unity для каждого стиля существования, описанного в таблице 14-2, есть свой
LifetimeManager
. Но, как вы впоследствии увидите в разделе 14.2.2, можно создать и свой собственный
LifetimeManager
Несмотря на то, что стиль Transient используется по умолчанию, мы можем задать это явным образом. Приведенные ниже примеры эквивалентны: container.RegisterType(); container.RegisterType( new TransientLifetimeManager());
Благодаря использованию технологии конфигурирования в коде мы можем регистрировать компоненты, имеющие различные стили существования, любым способом. Несмотря на то, что это намного более гибкий способ конфигурирования компонентов, иногда для получения позднего связывания нам необходимо прибегать к
XML. В этом случае мы также можем объявлять стили существования.

547
Конфигурирова ние стиля существования с помощью XML
Когда нам нужно определять компоненты в XML, нам также может понадобиться возможность конфигурировать в этом же месте их стили существования. Это можно легко сделать в рамках XML-схемы, которая уже рассматривалась в разделе 14.1.2
"Конфигурирование контейнера". Для объявления стиля существования можно использовать и необязательный элемент lifetime
:



Отличие от примера из раздела 14.1.2 "Конфигурирование контейнера" заключается в том, что теперь вы добавили необязательный элемент lifetime для того, чтобы определить, какой из
LifetimeManager должен использоваться для регистрации. Чтобы сконфигурировать компонент в виде Singleton, вы устанавливает е атрибут типа, равным псевдониму
ContainerControlledLifetimeManager
, но вместо него могли бы использовать и квалифицированное имя типа сборки или пользовательский псевдоним, если бы вам нужно было присвоить пользовательский
LifetimeManager
Конфигурировать жизненные циклы легко как в коде, так и в XML. В обоих случаях конфигурирование жизненных циклов выполняется в довольно декларативной манере.
Несмотря на то, что конфигурация выполняется довольно просто, вы не должны забывать о том, что некоторые стили существования содержат объекты-долго жители, которые пользуются ресурсами на протяжении всего своего жизненного цикла.
Высвобожде ние компонентов
Как уже говорилось в разделе 8.2.2 "Управление устраняемыми зависимостями", важно высвободить объекты после завершения работы с ними, чтобы каждый устраняемый экземпляр можно было бы устранить по истечении его жизненного цикла. Это возможно, но в рамках контейнера Unity сделать это довольно-таки трудно.
П редупреждение
Контейнер Unity не уничтожает устраняемые зависимости до тех пор, пока кто-нибудь явно не прикажет ему это сделать.
IUnityContainer определяет метод
Teardown
, который с первого взгляда кажется похожим на эквивалентный метод
Release контейнера Castle Windsor. Мы можем попробовать использовать его таким же образом: container.Teardown(ingredient);
Однако независимо от того, какой из встроенных стилей существования мы выбрали, ни один из компонентов не уничтожается. Это приводит к нарушению принципа "наименьшего удивления" (Principle of Least Surprise).
П редупреждение
Метод
Teardown не уничтожает устраняемые зависимости.

548
Несмотря на то, что метод
Teardown не выполняет (по умолчанию) то, что нам хотелось бы, нам, тем не менее, доступны некоторые другие варианты. Один из таких вариантов – реализовать пользовательский стиль существования (что вы и сделаете в следующем разделе). Еще один вариант – использовать комбинацию дочерних контейнеров и стиля существования Hierarchical.
Сущность стиля существования Hierarchical заключается в том, что в рамках дочернего контейнера он выступает в роли Singleton, но при этом каждый дочерний контейнер обладает своим собственным локальным Singleton.
Примечание
Комбинация дочерних контейнеров и стиля существования Hierarchical аналогична областям применения контейнера Autofac, описанным в разделе 13.2.1
"Конфигурирование областей применения экземпляров".
Дочерний контейнер – это копия родительского контейнера. При создании дочернего контейнера из родительского дочерний контейнер наследует из него всю конфигурацию, но впоследствии мы можем изменять дочерний контейнер, не влияя при этом на родительский. Такая возможность может быть полезной, если нам необходимо переопределить только небольшую часть конфигурации родительского контейнера.
Дочерний контейнер чаще всего имеет более ограниченную область применения. Как показывает рисунок 14-4, он также определяет границу, в пределах которой могут повторно использоваться компоненты.
Рисунок 14-4: Дочерние контейнеры могут совместно использовать компоненты в течение ограниченного периода или для ограниченного круга целей. Компонент
Hierarchical по существу играет роль Singleton в рамках этого контейнера. Независимо от того, сколько раз мы запрашиваем у дочернего контейнера этот компонент, мы получаем один и тот же экземпляр. Другой дочерний контейнер будет получать свой собственный экземпляр, а родительский контейнер управляет совместно используемыми
Singleton'ами. Transient-компоненты нельзя использовать совместно.

549
При создании нового дочернего контейнера он наследует все Singleton'ы, которыми управляет родительский контейнер, но при этом выступает в роли контейнера "локальных
Singleton'ов". Когда из дочернего контейнера запрашивается компонент Hierarchical, мы всегда получаем один и тот же экземпляр. Отличие от истинных Singleton'ов заключается в том, что, если мы запросим компонент Hierarchical у второго дочернего контейнера, то получим совсем другой экземпляр.
Однако T ransient -компоненты функционируют так, как и должны, независимо от того, разрешаем ли мы их из родительского или из дочернего контейнера.
П одсказка
Дочерние контейнеры и стиль существования Hierarchical можно использовать в качестве еще одного варианта опускания стиля существования Web Request Cont ext: создайте новый дочерний контейнер в начале каждого веб-запроса и используйте его для разрешения компонентов. После завершения запроса уничтожьте дочерний контейнер.
Одной из важных особенностей дочерних контейнеров является то, что они позволяют нам соответствующим образом высвобождать компоненты по истечении их жизненного цикла. С помощью метода
CreateChildContainer мы создаем новый дочерний контейнер и высвобождаем все соответствующие компоненты посредством вызова метода
Dispose
:
1.
using (var child = container.CreateChildContainer()
2.
{
3.
var meal = child.Resolve();
4.
}
Строка 3: Уничтожение обеда
Новый дочерний контейнер создается из container посредством вызова метода
CreateChildContainer
. Возвращаемо е значение реализует интерфейс
IDisposable
, поэтому вы можете поместить его в директиву using
. Получаем новый экземпляр
IUnityContainer
. В связи с этим вы можете использовать child для того, чтобы разрешать компоненты точно таким же способом, как и при использовании родительского контейнера.
После окончания работы с дочерним контейнером вы можете уничтожить его. При использовании директивы using дочерний контейнер автоматически уничтожается при выходе из этой директивы. Но, безусловно, вы можете сделать это и, явно уничтожив дочерний контейнер посредством вызова метода
Dispose
. При уничтожении child вы также высвобождаете все компоненты, созданные дочерним контейнером. В случае приведенного выше примера это означает, что вы высвобождает е диаграмму объекта meal
П римечание
Не забывайте, что высвобождение устраняемого компонента и его уничтожение – это не одно и то же. Это сигнал контейнеру о том, что срок эксплуатации этого компонента завершился. Если это Hierarchical-компо нент, то он уничтожится автоматически, а если
Singleton, то он не будет уничтожен автоматически.

550
П редупреждение
Устраняемые объекты со стилями существования T ransient или Per Graph не уничтожаются при уничтожении дочернего контейнера. Это может привести к утечкам памяти.
Ранее в этом разделе вы уже видели, как сконфигурировать компоненты в виде Singleton или T ransient . Конфигурирование компонента в виде Hierarchical выполняется аналогичным образом: container.RegisterType( new HierarchicalLifetimeManager());
При регистрации компонента с определенным стилем существования всегда используется перегрузка метода
RegisterType
, принимающая в качестве аргумента
LifetimeManager
Чтобы использовать стиль существования Hierarchical, вы передаете в метод экземпляр
HierarchicalLifetimeManager
По своей сущности Singleton'ы не высвобождаются по истечении жизненного цикла самого контейнера. Однако мы можем высвобождать даже такие компоненты, если контейнер нам больше не нужен. Делается это посредством уничтожения самого контейнера: container.Dispose();
На практике это не столь важно, поскольку жизненный цикл контейнера находится в близких взаимоотношениях с жизненным циклом поддерживаемого им приложения.
Обычно мы сохраняем контейнер активным до тех пор, пока приложение запущено.
Поэтому устраняем мы его только тогда, когда приложение закрывается, при этом операционная система восстанавливает память.
Стили существования, встроенные в контейнер Unity, могут показаться довольно исчерпывающим набором стилей существования, удовлетворя ющим практически всем поседневным нуждам. Но когда дело доходит до высвобождения компонентов, могут возникать проблемы в виде дефектов, а также проблемы несовместимости. С другой стороны, в контейнере Unit y присутствует достаточное количество Seam 'ов, чтобы можно было справиться с этими проблемами посредством разработки пользовательских стилей существования.
Разработка пользовательского стиля существования
В большинстве случаев мы должны уметь выходить из разных ситуаций с помощью исчерпывающего набора стилей существования, уже предоставляемых контейнером Unity, но если у нас имеются особые нужды или нам необходимо справиться с проблемами прекращения срока эксплуатации, можно реализовать пользовательский стиль существования. В данном разделе вы увидите, как это сделать. Мы рассмотрим
Seam
'ы, которые делают возможным создание пользовательского стиля существования, и часть времени потратим на рассмотрение примера, перемежая теорию с практикой.

551
Понимание API LifetimeManager
В разделе 14.2.1 "Конфигурирование стиля существования" мы уже мельком рассматривали API стилей существования контейнера Unity. Несколько перегрузок метода
RegisterType принимают в качестве параметра экземпляр абстрактного класса
LifetimeManager
, который моделирует процесс взаимодействия стилей существования с остальной частью контейнера Unity. На рисунке 14-5 продемонстрирована небольшая иерархия типов, связанная с классом
LifetimeManager
Рисунок 14-5:
SomeLifetimeManager реализует пользовательский стиль существования посредством наследования от абстрактного класса
LifetimeManager
, который, в свою очередь, реализует интерфейс
ILifetimePolicy
, унаследованный от интерфейса
IBuilderPolicy
. Пользовательский стиль существования может реализовать
IDisposable
, чтобы внедрить функциональность постобработки, в результате которой уничтожается контейнер.
При реализации пользовательского стиля существования важнейшим типом является абстрактный класс
LifetimeManager
. Даже если
LifetimeManager реализует
ILifetimePolicy
, это нас никоим образом не касается, поскольку перегрузки метода
RegisterType принимают в качестве параметра только экземпляры
LifetimeManager
, а не экземпляр ы
ILifetimePolicy или
IBuilderPolicy
Мы можем реализовать
IDisposable
, чтобы внедрить функциональность постобработки, но по умолчанию эта функциональность работает не так, как нам бы хотелось. Метод
Dispose вызывается не всегда. Позднее мы еще вернемся к рассмотрению этого вопроса.
Предупрежде ние
То, что мы реализуем
IDisposable
, еще не гарантирует, что будет вызван метод
Dispose
При разрешении компонента контейнер Unity взаимодействует с
LifetimeManager
, что проиллюстрировано на рисунке 14-6.

552
Рисунок 14-6: Контейнер Unity взаимодействует с интерфейсом
ILifetimePolicy
, вызывая сначала метод
GetValue
. Если policy возвращает какое-то значение, то это значение незамедлительно используется. Если значение не возвращается, то Unity создает новое значение и устанавливает его в policy перед тем, как вернуть это значение.
Примечание
Механизм, проиллюстрированный на рисунке 14-6, аналогичен взаимодейст вию
StructureMap с
IObjectCache
, которое продемонстрировано на рисунке 11-5.
Сначала Unity пытается получить запрашиваемый экземпляр из метода
GetValue
. Если этот метод возвращает null
, то Unity создает запрашиваемый экземпляр и добавляет его в policy с помощью метода
SetValue перед тем, как вернуть это значение. Таким образом, один экземпляр
ILifetimePolicy управляет одним компонентом.
Предупрежде ние
Метод
RemoveValue никогда не вызывается контейнером Unity.
Несмотря на то, что методы
GetValue и
SetValue принимают участие в процессе разрешения запроса контейнером Unity, метод
RemoveValue никогда не вызывается контейнером. Объяснение того, почему метод
Teardown не работает так, как нам бы хотелось, займет слишком времени. Мы могли бы оставить реализацию пустой, но, оказывается, мы можем изменить назначение метода. Перед детальным рассмотрением этого вопроса изучение примера, охватывающего самые основы, могло бы прояснить некоторые моменты.
Разработка стиля существования Caching
В приведенном ниже примере мы будем разрабатывать стиль существования Caching, который уже создавали для контейнеров Castle W indsor и StructureMap в разделах 10.2.3

553
"Р азработка пользовательского стиля существования" и 11.2.2 "Разработка пользовательского стиля существования". Если кратко, то этот стиль существования кэширует и повторно в течение некоторого времени использует экземпляры перед тем, как их высвободить.
Несмотря на то, что вы можете добавить некоторую дополнительную линию поведения, реализовав
IDisposable и применив некоторые уловки, вам нужно будет реализовать, как минимум, три абстрактных метода, определенных
LifetimeManager
. Это продемонстрировано в следующем листинге.
Листинг 14-3: Реализация пользовательского
LifetimeManager
1.
public partial class CacheLifetimeManager :
2.
LifetimeManager, IDisposable
3.
{
4.
private object value;
5.
private readonly ILease lease;
6.
public CacheLifetimeManager(ILease lease)
7.
{
8.
if (lease == null)
9.
{
10.
throw new ArgumentNullException("lease");
11.
}
12.
this.lease = lease;
13.
}
14.
public override object GetValue()
15.
{
16.
this.RemoveValue();
17.
return this.value;
18.
}
19.
public override void RemoveValue()
20.
{
21.
if (this.lease.IsExpired)
22.
{
23.
this.Dispose();
24.
}
25.
}
26.
public override void SetValue(object newValue)
27.
{
28.
this.value = newValue;
29.
this.lease.Renew();
30.
}
31.
}
Строка 14-18: Получение значения
Строка 19-25: Удаление значения
Строка 26-30: Установка значения
Чтобы реализовать стиль существования Caching, необходимо унаследовать класс
CacheLifetimeManager от абстрактного класса
LifetimeManager
. Кроме того, класс
CacheLifetimeManager реализует
IDisposable
, но мы немного повременим с изучением реализации, поэтому в листинге 14-3 метод
Dispose пропущен.
CacheLifetimeManager для получения экземпляра
ILease использует паттерн Constructor
Injection. Интерфейс
ILease
– это локальный вспомогательный интерфейс, который вводится для реализации необходимой функциональности. Впервые этот интерфейс был

554 введен в разделе 10.2.3 "Разработка пользовательского стиля существования" и никак не влияет на контейнер Unity или любой другой DI-контейнер.
П римечание
Пример реализации
ILease можно увидеть в разделе 10.2.3 "Разработка пользовательского стиля существования".
Метод
GetValue сначала вызывает метод
RemoveValue
, чтобы обезопасить себя от недействительного срока аренды, а затем возвращает значение поля value
. Поле value может иметь null
-значение, но, как демонстрирует рисунок 14-6, это ожидаемый сценарий. С другой стороны, в поле может содержаться значение, если сначала был вызван метод
SetValue и при этом срок аренды не просрочен.
Несмотря на то, что метод
RemoveValue никогда не вызывается самим Unity, это все равно отличное место для реализации кода, позволяющего высвободить компонент. Поскольку целью
CacheLifetimeManager является кэширование значения на некоторое время, вы устраняете компонент только по окончании срока аренды. В противном случае вы храните его несколько дольше. Метод
Dispose не включен в листинг 14-3, но мы скоро к нему вернемся.
Метод
SetValue сохраняет значение в поле value и продляет срок аренды. Согласно схеме, приведенной на рисунке 14-6, метод
SetValue вызывается только тогда, когда
Unity создает новое значение для рассматриваемого компонента, причем в этом случае уместно продлять срок аренды.
П римечание
Сравните конструктор из листинга 14-3 с более сложным кодом, приведенным в листинге
10.2. Это сравнение отчетливо демонстрирует превосходство паттерна Constructor
Injection над Method Injection.
Все это реализует ключевую функциональност ь, необходимую для
LifetimeManager
Хотя нам все равно нужно обсудить реализацию
IDisposable и то, что под ним подразумевается, нам следует для начала вкратце рассмотреть то, как
CacheLifetimeManager сопоставляется с экземпляром
UnityContainer
Ре гистрация компонентов с пользовательским стилем существования
Применять
CacheLifetimeManager в рамках компонента довольно легко и делается это наподобие определения всех остальных стилей существования: var lease = new SlidingLease(TimeSpan.FromMinutes(1)); var cache = new CacheLifetimeManager(lease); container.RegisterType
1   ...   33   34   35   36   37   38   39   40   ...   43


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