Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
расширения контейнера (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 В этом примере конкретный класс SauceBéarnaise конфигурируется в виде Singleton таким образом, что всякий раз при запрашивании SauceBéarnaise возвращается один и тот же экземпляр. Если нам необходимо преобразовать абстракцию в конкретный класс, имеющий конкретный стиль существования, можно воспользоваться еще одной похожей перегрузкой метода RegisterType : container.RegisterType В этом примере IIngredient преобразуется в SauceBéarnaise и конфигурируется в виде Singleton. В двух предыдущих примерах вы использовали перегрузки метода RegisterType , которые в качестве аргумента принимали экземпляр LifetimeManager Вместо ContainerControlledLifetimeManager вы можете использовать любой другой класс, унаследованный от абстрактного класса LifetimeManager . В контейнере Unity для каждого стиля существования, описанного в таблице 14-2, есть свой LifetimeManager . Но, как вы впоследствии увидите в разделе 14.2.2, можно создать и свой собственный LifetimeManager Несмотря на то, что стиль Transient используется по умолчанию, мы можем задать это явным образом. Приведенные ниже примеры эквивалентны: container.RegisterType Благодаря использованию технологии конфигурирования в коде мы можем регистрировать компоненты, имеющие различные стили существования, любым способом. Несмотря на то, что это намного более гибкий способ конфигурирования компонентов, иногда для получения позднего связывания нам необходимо прибегать к 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 При регистрации компонента с определенным стилем существования всегда используется перегрузка метода 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 |