Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
Является ли MEF DI-контейнер'ом? Вокруг того, является ли MEF DI-контейнером или нет, возникает слишком много путаницы. Если вкратце, то нет, MEF не является DI-контейнером, но у него настолько много общего с "настоящими" DI-контейнерами, что в будущем он может стать полноценным DI-контейнером. MEF не создавался как DI-контейнер. Его целью является предоставление универсального фреймворка для подключения дополнительной функциональности в стандартные приложения. С точки зрения стандартного приложения, расширение – это неизвестный компонент. Несмотря на то, что расширение, по всей видимости, необходимо для раскрытия определенного интерфейса, это и есть все, что знает о нем приложение. В зависимости от среды разработки в приложении могут присутствовать одно или несколько расширений, или же в приложение может не входить ни одного расширения. Эти MEF 584 отличается от DI-контейнера, в котором мы обычно на этапе компиляции бываем осведомлены обо всех (или о большинстве) его компонентах. Когда мы используем DI-контейнер в качестве инструмента для компоновки приложения, мы точно знаем, из каких компонентов состоит приложение, и используем эти знания для того, чтобы сконфигурировать контейнер в Com position Root приложения. С другой стороны, когда дело касается плагинов, мы знаем только о том, что плагины должны реализовывать некоторого рода абстракцию, но не можем скомпилироват ь приложение, состоящее из конфигурации конкретных плагинов, поскольку на этапе разработки они еще неизвестны. В этом случае нам необходим механизм, позволяющий обнаруживать компоненты приложения. Т радиционный механизм обнаружения расширений заключается в просмотре определенного каталога сборок с целью обнаружения всех классов, реализующих необходимую абстракцию. Однако это не решает той проблемы, которая возникает, когда у самого расширения есть зависимости. С другой стороны, MEF решает именно эту проблему посредством расширенной модели обнаружения, которая для определения потребителей и их сервисов использует атрибуты. DI-контейнер поддерживает раздельную компоновку сервисов. Т акая возможность обеспечивает высокую степень гибкости, но является довольно затратной: будучи разработчиками уже на этапе конфигурирования контейнера мы должны быть осведомлены о том, какие компоненты собираемся компоновать. MEF поддерживает механизм обнаружения ком понент ов. Такой подход успешно решает проблему, возникающу ю в ситуациях, когда на этапе проектирования мы почти ничего не знаем о расширениях. Компромисс заключается в том, что механизм обнаружения расширений сильно связан с компонентами, поэтому мы теряем некоторую гибкость. При рассмотрении внутренней инфраструктуры MEF, оказывается, что процессы обнаружения расширений и компоновки не связаны друг с другом. Это означает, что компания Microsoft может усовершенство вать MEF в направлении настоящего DI- контейнера. С другой стороны, некоторые DI-контейнер'ы обладают столь огромным багажом основанных на соглашениях возможностей, что в будущем могут посягнуться на MEF. Даже на сегодняшний день MEF имеет столь много общего с DI-контейнерами, что некоторые считают его DI-контейнером, а некоторые отвергают этот факт. П одсказка Если вы уже пытались использовать MEF в качестве DI-контейнера и были разочарованы и сбиты с толку, то эта глава объяснит вам, почему так произошло. Несмотря на то, что, когда MEF зарождался и разрабатывался, возможность использования его в качестве DI-контейнера не имела первостепенной важности, оказывается, его все-таки можно использовать как DI-контейнер. Со временем это стало возможным, хотя и довольно затруднительным. Существует несколько сценариев, при которых имеет смысл использовать MEF в качестве DI-контейнера: в частности, в приложениях, в которых он уже используется для реализации возможностей расширяемости приложения. 585 Примечание Общая предпосылка этой главы (непреднамеренно) заключается в том, что использование MEF не принесет нам успешного результата. Это не означает, что MEF делает что-то не так. Это означает, что мы пытаемся заставить MEF сделать то, для чего он не предназначен. Мы пытаемся сделать невозможное. В этой главе мы рассмотрим, как можно использовать MEF для применения принципов и паттернов, описанных в частях 1-3. На рисунке 15-1 продемонстрирована структура этой главы. Рисунок 15-1: Эта глава состоит из четырех разделов. Первый раздел знакомит нас с MEF и демонстрирует способы конфигурирования и разрешения компонентов. Следующие три раздела связаны с паттернами применения, которые требуют дополнительного внимания. Их можно прочитать по порядку или же пропустить некоторые из этих разделов и прочитать только те, которые вас интересуют. Структура главы является отражением структуры всех остальных глав части 4, поскольку я считаю, что это облегчает сравнение различных DI-контейнеров. В случае с MEF это приводит к четырем неравноценным по размеру и важности разделам. Много времени мы потратим на знакомство с M EF для того, чтобы получить полноценное понимание этого контейнера и его API. На рассмотрение механизма управления жизненным циклом выделим только несколько страниц, поскольку в этой области MEF не обладает обширными возможностями. Далее большая часть оставшейся главы отведена на изучение возможностей работы с составными компонентами, а последний раздел снова занимает совсем немного страниц. Первый раздел является предпосылко й остальных разделов, оставшиеся три раздела можно читать независимо друг от друга. Эта глава должна дать вам возможность приступить к работе, а также справиться с большинством общепринятых проблем, которые могут возникнуть при использовании MEF. Однако это не окончательная трактовка MEF, поскольку мы рассматриваем его в терминах DI-контейнеров, а не с точки зрения его реального предназначения. Вы можете прочитать эту главу, не читая при этом остальные главы части 4, специально для того, чтобы познакомиться с M EF, или можете прочитать ее совместно с остальными главами части 4, чтобы сравнить MEF с "настоящими" DI-контейнерами. Цель данной главы – показать, как MEF связан с паттернами и принципами, описанными в остальной части книги, и как он их реализует. 15.1. Знакомство с MEF 15.2. Управление жизненным циклом 586 15.3. Работа с составными компонентами 15.4. Конфигурирование сложных API 15.5. Резюме 587 15.1. Знакомство с MEF Из этого раздела вы узнаете, где можно взять MEF, что вы при этом получите и как начать его использовать. Кроме того, мы рассмотрим варианты конфигурирования и пакетирования компонентов. В таблице 15-1 содержится основополагающая информация, которая, скорее всего, понадобится вам для того, чтобы приступить к работе с Unity. Таблица 15-1: Краткая информация об MEF Вопрос Ответ Откуда мне его получить? MEF является частью .NET 4 и Silverlight 4. Что находится в загруженном файле? Вы получаете MEF при установке .NET 4 или Silverlight 4. M EF является частью стандартной библиотеки классов и упакован в сборку System.ComponentModel.Composition Если вы посетите сайт ht tp://mef.codeplex.com/ , то сможете также загрузить исходный код для дальнейшего внимательного изучения. Какие платформы поддерживаются? .NET 4 и Silverlight 4. На сайте ht tp://mef.codeplex.com / вы также можете найти неподдерживаемые версии для .NET 3.5 SP1 и Silverlight 3. Сколько он стоит? Нисколько. MEF входит в состав .NET 4 и Silverlight 4. Откуда мне получить помощь? Поскольку MEF является частью .NET 4 и Silverlight 4, вы можете обратиться за поддержкой в компанию Microsoft. На какой версии MEF основана эта глава? .NET 4 В отличие от остальных DI-контейнеров MEF имеет другой цикл применения. Мы никогда не конфигурируем контейнер, зато снабжаем сами компоненты атрибутами. На рисунке 15-2 продемонстрирована взаимосвязь компонентов с движком компоновки. Рисунок 15-2: При работе с MEF мы снабжаем части (к примеру, классы и члены) атрибутами в рамках отдельных рабочих фаз. При компоновке приложения мы сначала отбираем соответствующие части в каталог, а затем используем этот каталог для определения контейнера, из которого можно разрешать компоненты. 588 Терминология MEF MEF использует терминологию, которая слегка отличается от той, которой мы пользовались при обсуждении других DI-контейнеров. Обычно взаимодейству ющие классы мы называли компонент ами, но в MEF для обозначения таких классов используется термин част ь. Част ь – это класс или член класса, который передает или использует зависимость. Когда часть использует зависимость, мы говорим, что она импортирует ее. Напротив, когда часть предоставляет сервис, она его экспорт ирует . В классическо м случае в профессионально й сфере слова import и export могут использоваться и как существительные: импорт и экспорт. Возможность экспорта и импорта определяется путем снабжения частей соответствующими атрибутами. При компоновке приложения мы сопоставляем экспортируемые и импортируемые компоненты в соответствии с контракт ами. Часто в качестве контрактов мы используем типы (например, интерфейсы), но MEF более гибкий, нежели контракты. Контракт, в действительности, – это всего лишь строка. При работе с другими DI-контейнерами мы используем расщепленное API конфигурации для определения доступных компонентов, для определения того, как конкретные типы преобразуются в абстракции, как создаются компоненты, и должны ли экземпляры использоваться совместно. Напротив, при работе с MEF мы привязываем эту информацию к каждой части путем применения атрибутов к типам и членам. Это довольно легко понять, но такой подход сильно связывает конфигурацию компонента с самим компонентом. П римечание Помните о том, что MEF, по первой версии, использует атрибуты в качестве метода обнаружения по умолчанию (и только по умолчанию), но по сути, не привязан к атрибутам как к средству обнаружения. Чтобы скомпоновать приложение, мы отбираем соответствующие части и пакетируем их в каталог, а затем создаем контейнер, который может разрешать компоненты из этого каталога. После прочтения этого раздела вы приобретете полноценное понимание всей сущности паттерна применения MEF и сможете начать использовать его в сценариях, в которых все части определяют простые экспортируемые и импортируемые компоненты. Давайте начнем с простейшего сценария и посмотрим, как можно разрешать объекты с помощью контейнера MEF. Разрешение объектов Основная услуга, предоставляемая любым DI-контейнером – компоновка и разрешение компонентов. В этом разделе мы рассмотрим API, которое позволяет разрешать компоненты с помощью MEF. Что касается работы с любым другим контейнером, 589 разрешать объекты в них так же просто, как и вызывать простой метод. Но при работе с MEF мы не можем что-либо разрешать до тех пор, пока не станут доступными соответствующие экспортируемые компоненты. Вспоминая обсуждение процесса разрешения компонентов при помощи Castle Windsor и StructureMap, вы, возможно, припомните, что Windsor требует, чтобы перед тем, как стало возможным разрешение компонентов, все соответствующие компоненты были зарегистрированы. MEF предъявляет аналогичное требование, несмотря на то, что мы никоим образом не можем зарегист рироват ь компонент. Вместо этого часть должна экспортировать необходимый сервис. Чтобы разрешить сервис SauceBéarnaise , мы должны его экспортировать. Самый простой способ осуществить это – путем аннотирования самого класса следующим образом: [Export] public class SauceBéarnaise : IIngredient { } Обратите внимание на атрибут [Export] , которым снабжен класс SauceBéarnaise . Это MEF атрибут, который объявляет, что класс SauceBéarnaise экспортирует самого себя. Это означает, что, если вы поместите этот класс в каталог, то после этого сможете разрешать класс SauceBéarnaise , но никакой другой, поскольку атрибутом экспорта отмечен только этот класс: var catalog = new TypeCatalog(typeof(SauceBéarnaise)); var container = new CompositionContainer(catalog); SauceBéarnaise sauce = container.GetExportedValue На рисунке 15-2 вы уже видели некоторый намек на понятие каталога. Более детально мы рассмотрим его в разделе 15.1.3. А сейчас будет достаточно заметить, что аннотированный класс SauceBéarnaise помещается в каталог, который вы используете для определения контейнера. После получения контейнера вы можете использовать его для разрешения сервиса SauceBéarnaise П римечание Метод GetExportedValue полностью соответствует методам Resolve контейнеров W indsor, Autofac и Unit y. Помимо метода GetExportedValue MEF поддерживает и еще один стиль, при котором мы сначала получаем экспортируемые компоненты, а затем извлекаем значение этих компонентов. Самая простая форма этого стиля выглядит следующим образом: Lazy SauceBéarnaise sauce = export.Value; Метод GetExport – отличный пример экспорта, который является первостепенным понятием MEF. Метод GetExport инкапсулирует экспортируемые компоненты, не создавая при этом экземпляр части. Создание части можно отложить до тех пор, пока мы не запросим свойство Value этой части, но все это зависит от стиля существования части. 590 И метод GetExportedValue , и метод GetExport обладают многочисленными аналогами, что позволяет нам разрешать последовательности частей. Выглядят они следующим образом: IEnumerable IEnumerable До настоящего момента класс SauceBéarnaise экспортировал только свой собственный тип. Даже если он реализует IIngredient , он все равно не будет экспортировать этот интерфейс до тех пор, пока вы явно не укажете ему на это. При преобразовании абстракций к конкретным типам также используется атрибут [Export] П реобразование абстракций в конкретны е типы Атрибут [Export] экспортирует ту часть, к которой он относится. Иногда экспортируемая часть уже является абстракцией, но при аннотировании класса по умолчанию экспортируется конкретный класс, даже если он реализует один или несколько интерфейсов. В связи со слабым связыванием нам необходимо преобразовывать абстракции в конкретные типы. Создание экземпляров на основании таких преобразований – основная услуга, предоставляемая любым DI-контейнером, но нам все равно нужно определить преобразование. При работе с MEF мы делаем это путем явного указания в атрибуте [Export] того, что он должен экспортировать. В приведенном ниже примере вы позволяете конкретному классу SauceBéarnaise экспортировать интерфейс IIngredient : [Export(typeof(IIngredient))] public class SauceBéarnaise : IIngredient { } По сравнению с предыдущим примером вы изменили атрибут [Export] таким образом, что теперь используется перегрузка, которая позволяет нам указывать, что экспортируемым типом является IIngredient . И снова вы пакетируете класс SauceBéarnaise в каталог, а затем из этого каталога создаете контейнер. IIngredient ingredient = container.GetExportedValue При разрешении IIngredient из контейнера, оказывается, что значение ingredient , как и ожидалось, является экземпляром класса SauceBéarnaise . Однако если вы попытаетесь разрешить SauceBéarnaise таким образом, как делали это в первом примере, то получите исключение, в связи с отсутствием частей, которые экспортировали бы контракт SauceBéarnaise Вы легко можете это сделать, несколько раз применив атрибут [Export] : [Export] [Export(typeof(IIngredient))] public class SauceBéarnaise : IIngredient { } 591 Атрибут [Export] можно применять столько раз, сколько потребуется, поэтому при таком варианте экспортируется и класс SauceBéarnaise , и интерфейс IIngredient До настоящего момента мы рассматривали строго типизированный generic-метод, используемый для разрешения сервисов. Однако существуют ситуации, когда для разрешения сервисов, нам необходим более слабо типизированный подход. Это возможно, но подразумевает выполнение несколько большего количества действий, чем планировалось. Разрешение слабо типизированных сервисов Иногда у вас нет возможности использовать generic API, поскольку на этапе проектирования вы еще не знаете, какой тип вам понадобится. У вас есть только экземпляр Type , но все же вам хотелось бы получить экземпляр этого типа. Пример такой ситуации описан в разделе 7.2 "Построение ASP.NET MVC приложений", в котором обсуждался ASP.NET класс DefaultControllerFactory . Соответствующий метод приведен в следующем примере: protected internal virtual IController GetControllerInstance( RequestContext requestContext, Type controllerType); Поскольку у вас имеется только экземпляр Type , вы не можете использовать generic'и, а должны прибегнуть к слабо типизированному API. К несчастью, единственное нетипизированно е API, раскрываемое с помощью CompositionContainer , является слегка громоздким. Нетипизированных версий методов GetExportedValue или GetExportedValues не существует, поэтому для реализации GetControllerInstance нам необходимо прибегнуть к не generic-версии GetExports : var export = this.container.GetExports( controllerType, null, null).Single(); return (IController)export.Value; Существует несколько перегрузок метода GetExports , но в этом примере мы используем ту, которая позволяет нам передавать результат напрямую в controllerType . Два других параметра можно использовать для создания ограничителей запроса, но если они нам не нужны, то мы можем передать для них значения типа null . М етод GetExports возвращает последовательност ь экспортируемых компонентов, но согласно нашему условию экспортировать можно только один компонент, удовлетворяющий параметрам запроса. Поэтому для получения единичного экземпляра из последовательнос ти мы вызываем метод расширения Single Поскольку метод GetExports является слабо типизированным, мы должны привести экспортируемое значение к IController перед тем, как его вернуть. В любом случае независимо от того, какой конкретный метод вы используете для разрешения частей, MEF компонует части посредством сопоставления импортируемых и экспортируемых компонентов. MEF может это делать только, когда мы заранее явно определили это. Определение импортируемых и экспортируемых компонентов В разделе 3.2 "Конфигурирование DI-контейнеров" мы обсуждали несколько различных способов конфигурирования DI-контейнера. На рисунке 15-3 представлен обзор 592 возможных вариантов, а также показано то, что MEF совершенно не подходит под эту модель. Рисунок 15-3: MEF не подходит под нашу стандартную концептуальную модель вариантов конфигурирования, поскольку мы не можем конфигурировать контейнер. Все обычные варианты, которые мы рассматривали ранее, являются недоступными и обозначены серым цветом. Снабжение частей атрибутами статистически наряду с типом определяет конфигурацию, тогда как каталоги обеспечивают гибкость. Мы не можем сконфигурировать контейнер с помощью используемых нами ранее способов – ни с помощью императивного кода, ни посредством XML конфигурации. MEF дает нам возможность определять только импортируемые и экспортируемые части, а именно посредством применения атрибутов к этим частям. Атрибуты являются частями тех типов, которыми они отмечены, поэтому мы должны рассматривать этот механизм как более явный и более рано связанный, нежели, например, технология конфигурирования в коде. С другой стороны, каталоги делают MEF довольно гибким, поскольку позволяют отбирать типы, которые мы хотим включить в компоновку. Предупрежде ние Применение атрибутов сильно привязывает конфигурацию к реализации. В разделе 9.3.1 "Использование атрибутов для объявления аспектов" мы обсуждали недостатки использования атрибутов для аспектов, но это обсуждение применимо и ко всему 593 остальному. Не забывайте, что в будущем в MEF может появиться некоторая альтернатива атрибутам. В этом разделе мы рассмотрим варианты импорта и экспорта частей, а затем в разделе 15.1.3 перейдем к изучению каталогов. Несмотря на то, что мы не можем использовать наши стандартные варианты конфигурирования контейнера, мы можем привести некоторые соответствия, чтобы рассмотреть другие сценарии. В этом разделе вы познакомитесь с различными способами экспорта и импорта частей. Экспорт типов В этом разделе мы рассмотрим сценарий, при котором мы управляем классами, которые планируем экспортировать. Когда мы полностью контролируем исходный код тех классов, которые собираемся экспортировать, мы можем экспортировать класс с помощью атрибута [Export] : [Export] [Export(typeof(IIngredient))] public class SauceBéarnaise : IIngredient { } Свойство [Export] можно применять столько раз, сколько потребуется, поэтому один и тот же класс может экспортировать различные контракты. Класс SauceBéarnaise , продемонстрированный в этом примере, экспортируется и как конкретный класс, и как интерфейс IIngredient Перегрузка [Export] , позволяющая указывать экспортируемый тип, не нуждается в проверке на этапе компиляции. Вы можете объявить невалидный атрибут экспорта, и при этом не произойдет никаких ошибок компиляции: [Export(typeof(ICourse))] public class SauceBéarnaise : IIngredient { } Класс SauceBéarnaise не реализует интерфейс ICourse , хотя вы можете составить требование, что он должен его реализовывать. Однако при попытке разрешения ICourse выдается исключение, поскольку MEF не умеет приводить SauceBéarnaise к ICourse П редупреждение Можно объявлять невалидные атрибуты экспорта. Очевидно, вы можете позволить различным классам экспортировать разные контракты, и при этом не возникнет никаких конфликтов: [Export(typeof(IIngredient))] public class SauceBéarnaise : IIngredient { } [Export(typeof(ICourse))] public class Course : ICourse { } Поскольку каждый отдельный класс экспортирует отличные от других контракты, конфликта не возникает, и вы можете отправить контейнеру запрос на разрешение и ICourse , и IIngredient , получив в итоге экземпляры Course и SauceBéarnaise соответственно. 594 Однако если мы экспортируем одну и ту же абстракцию несколько раз, то картина изменится: [Export(typeof(IIngredient))] public class SauceBéarnaise : IIngredient { } [Export(typeof(IIngredient))] public class Steak : IIngredient { } В этом примере вы экспортируете IIngredient дважды. Если вы попытаетесь разрешить IIngredient , то контейнер выдаст исключение, поскольку в коде приведено несколько атрибутов экспорта. Под вызовом метода GetExport или GetExportedValue подразумевается запрос повсеместно встречающейся части. Вы все равно можете получить и SauceBéarnaise , и Steak , вызвав множественные методы GetExports или GetExportedValues П римечание В MEF нет такого понятия как |