Главная страница   /   15.1. Знакомство с MEF (Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Внедрение зависимостей в .NET

Марк Симан

15.1. Знакомство с MEF

Из этого раздела вы узнаете, где можно взять MEF, что вы при этом получите и как начать его использовать. Кроме того, мы рассмотрим варианты конфигурирования и пакетирования компонентов. В таблице 15-1 содержится основополагающая информация, которая, скорее всего, понадобится вам для того, чтобы приступить к работе с Unity.

Таблица 15-1: Краткая информация об MEF
Вопрос Ответ
Откуда мне его получить? MEF является частью .NET 4 и Silverlight 4.
Что находится в загруженном файле?

Вы получаете MEF при установке .NET 4 или Silverlight 4. MEF является частью стандартной библиотеки классов и упакован в сборку System.ComponentModel.Composition.

Если вы посетите сайт http://mef.codeplex.com/, то сможете также загрузить исходный код для дальнейшего внимательного изучения.

Какие платформы поддерживаются?

.NET 4 и Silverlight 4.

На сайте http://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 мы снабжаем части (к примеру, классы и члены) атрибутами в рамках отдельных рабочих фаз. При компоновке приложения мы сначала отбираем соответствующие части в каталог, а затем используем этот каталог для определения контейнера, из которого можно разрешать компоненты.

Терминология MEF

MEF использует терминологию, которая слегка отличается от той, которой мы пользовались при обсуждении других DI-контейнеров.

Обычно взаимодействующие классы мы называли компонентами, но в MEF для обозначения таких классов используется термин часть. Часть – это класс или член класса, который передает или использует зависимость.

Когда часть использует зависимость, мы говорим, что она импортирует ее. Напротив, когда часть предоставляет сервис, она его экспортирует. В классическом случае в профессиональной сфере слова import и export могут использоваться и как существительные: импорт и экспорт.

Возможность экспорта и импорта определяется путем снабжения частей соответствующими атрибутами.

При компоновке приложения мы сопоставляем экспортируемые и импортируемые компоненты в соответствии с контрактами. Часто в качестве контрактов мы используем типы (например, интерфейсы), но MEF более гибкий, нежели контракты. Контракт, в действительности, – это всего лишь строка.

При работе с другими DI-контейнерами мы используем расщепленное API конфигурации для определения доступных компонентов, для определения того, как конкретные типы преобразуются в абстракции, как создаются компоненты, и должны ли экземпляры использоваться совместно.

Напротив, при работе с MEF мы привязываем эту информацию к каждой части путем применения атрибутов к типам и членам. Это довольно легко понять, но такой подход сильно связывает конфигурацию компонента с самим компонентом.

Примечание

Помните о том, что MEF, по первой версии, использует атрибуты в качестве метода обнаружения по умолчанию (и только по умолчанию), но по сути, не привязан к атрибутам как к средству обнаружения.

Чтобы скомпоновать приложение, мы отбираем соответствующие части и пакетируем их в каталог, а затем создаем контейнер, который может разрешать компоненты из этого каталога.

После прочтения этого раздела вы приобретете полноценное понимание всей сущности паттерна применения MEF и сможете начать использовать его в сценариях, в которых все части определяют простые экспортируемые и импортируемые компоненты. Давайте начнем с простейшего сценария и посмотрим, как можно разрешать объекты с помощью контейнера MEF.

Разрешение объектов

Основная услуга, предоставляемая любым DI-контейнером – компоновка и разрешение компонентов. В этом разделе мы рассмотрим API, которое позволяет разрешать компоненты с помощью MEF. Что касается работы с любым другим контейнером, разрешать объекты в них так же просто, как и вызывать простой метод. Но при работе с 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<SauceBéarnaise>();

На рисунке 15-2 вы уже видели некоторый намек на понятие каталога. Более детально мы рассмотрим его в разделе 15.1.3. А сейчас будет достаточно заметить, что аннотированный класс SauceBéarnaise помещается в каталог, который вы используете для определения контейнера. После получения контейнера вы можете использовать его для разрешения сервиса SauceBéarnaise.

Примечание

Метод GetExportedValue полностью соответствует методам Resolve контейнеров Windsor, Autofac и Unity.

Помимо метода GetExportedValue MEF поддерживает и еще один стиль, при котором мы сначала получаем экспортируемые компоненты, а затем извлекаем значение этих компонентов. Самая простая форма этого стиля выглядит следующим образом:

Lazy<SauceBéarnaise> export =
	container.GetExport<SauceBéarnaise>();
SauceBéarnaise sauce = export.Value;

Метод GetExport – отличный пример экспорта, который является первостепенным понятием MEF. Метод GetExport инкапсулирует экспортируемые компоненты, не создавая при этом экземпляр части. Создание части можно отложить до тех пор, пока мы не запросим свойство Value этой части, но все это зависит от стиля существования части.

И метод GetExportedValue, и метод GetExport обладают многочисленными аналогами, что позволяет нам разрешать последовательности частей. Выглядят они следующим образом:

IEnumerable<IIngredient> ingredients =
	container.GetExportedValues<IIngredient>();
IEnumerable<Lazy<IIngredient>> exports =
	container.GetExports<IIngredient>();

До настоящего момента класс 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>();

При разрешении IIngredient из контейнера, оказывается, что значение ingredient, как и ожидалось, является экземпляром класса SauceBéarnaise. Однако если вы попытаетесь разрешить SauceBéarnaise таким образом, как делали это в первом примере, то получите исключение, в связи с отсутствием частей, которые экспортировали бы контракт SauceBéarnaise.

Вы легко можете это сделать, несколько раз применив атрибут [Export]:

[Export]
[Export(typeof(IIngredient))]
public class SauceBéarnaise : IIngredient { }

Атрибут [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 представлен обзор возможных вариантов, а также показано то, что MEF совершенно не подходит под эту модель.

Рисунок 15-3: MEF не подходит под нашу стандартную концептуальную модель вариантов конфигурирования, поскольку мы не можем конфигурировать контейнер. Все обычные варианты, которые мы рассматривали ранее, являются недоступными и обозначены серым цветом. Снабжение частей атрибутами статистически наряду с типом определяет конфигурацию, тогда как каталоги обеспечивают гибкость.

Мы не можем сконфигурировать контейнер с помощью используемых нами ранее способов – ни с помощью императивного кода, ни посредством XML конфигурации. MEF дает нам возможность определять только импортируемые и экспортируемые части, а именно посредством применения атрибутов к этим частям. Атрибуты являются частями тех типов, которыми они отмечены, поэтому мы должны рассматривать этот механизм как более явный и более рано связанный, нежели, например, технология конфигурирования в коде. С другой стороны, каталоги делают MEF довольно гибким, поскольку позволяют отбирать типы, которые мы хотим включить в компоновку.

Предупреждение

Применение атрибутов сильно привязывает конфигурацию к реализации. В разделе 9.3.1 "Использование атрибутов для объявления аспектов" мы обсуждали недостатки использования атрибутов для аспектов, но это обсуждение применимо и ко всему остальному. Не забывайте, что в будущем в 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 соответственно.

Однако если мы экспортируем одну и ту же абстракцию несколько раз, то картина изменится:

[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 нет такого понятия как компонент по умолчанию. Все экспортируемые компоненты равны между собой.

Это и есть первый намек на такое важное понятие MEF, как мощность импортируемых и экспортируемых компонентов. Количество экспортируемых компонентов должно совпадать с количеством импортируемых компонентов. В таблице 15-2 показано, как MEF сопоставляет импортируемые и экспортируемые компоненты по мощности.

Таблица 15-2: Сопоставление количества атрибутов импорта и экспорта
Export.Single Export.Many
Import.Single Равно Не равно
Import.Many Равно Равно

В этом контексте термин "many" используется для обозначения последовательности частей, обычно массива IEnumerable<T>. Если мы будем явным образом импортировать множество частей одного и того же контракта, то MEF всегда будет находить соответствие, поскольку нулевое количество атрибутов экспорта есть особый случай множественных атрибутов экспорта.

С другой стороны, явно импортируя один экземпляр, мы получаем несоответствие мощностей в случае наличия нулевого или множественного количества атрибутов экспорта, потому что импорт одного экземпляра означает, что у нас должен быть только один экземпляр.

Примечание

Мощность – это один из тех видов размерностей, по которым атрибуты экспорта и импорта должны соответствовать друг другу.

Как вы увидите в разделе 15.2.1, механизм управления жизненным циклом также может играть важную роль в вопросе соответствия частей, но мощность учитывается всегда. Далее в этом разделе вы увидите, как определять единичные и множественные атрибуты импорта. Но перед тем как приступить к рассмотрению этого вопроса, нам следует рассмотреть процесс экспорта частей в тех ситуациях, когда мы не контролируем классы, участвующие в этом процессе.

Экспорт адаптеров

Самый простой способ экспортировать часть – применить к классу атрибут [Export], но это не всегда возможно сделать. Нам может понадобиться экспортировать классы, которые уже были скомпилированы, а доступа к исходному коду у нас нет. В таких случаях мы не можем применять атрибуты, хотя нам и хотелось бы включить этот класс в компоновку.

И все равно мы можем достичь этой цели, усилив способность MEF экспортировать свойства так же, как и классы. Для примера рассмотрим конструктор Mayonnaise:

public Mayonnaise(EggYolk eggYolk, OliveOil oil)

Представьте себе, что класс Mayonnaise и составляющие его зависимости EggYolk и OliveOil не подвластны нам. Одним из возможных вариантов в таком случае было бы наследование от первоначального класса и применение атрибута [Export] к унаследованному классу:

[Export(typeof(OliveOil))]
[Export(typeof(IIngredient))]
public class MefOliveOil : OliveOil { }

Обратите внимание, что если вам необходимо экспортировать и первоначальный конкретный класс, и интерфейс IIngredient, то вы должны явным образом установить, что базовый класс (который и является конкретным классом) также должен быть экспортирован. Если бы вы использовали атрибут [Export] без указания типа, то вместо базового класса вы бы экспортировали класс MefOliveOil.

Однако если рассматриваемые классы заблокированы, то вы не сможете экспортировать их таким образом. В этом случае, как показано в следующем листинге, можно создать адаптер и экспортировать часть через свойство.

Листинг 15-1: Экспорт OliveOil через адаптер
public class OliveOilAdapter
{
	private readonly OliveOil oil;
	public OliveOilAdapter()
	{
		this.oil = new OliveOil();
	}
	[Export]
	public OliveOil OliveOil
	{
		get { return this.oil; }
	}
}

Строка 8: Экспорт свойства

Класс OliveOilAdapter – это совершенно новый класс, который обертывает первоначальный класс OliveOil и экспортирует его посредством аннотированного свойства. Атрибут [Export] можно применять как к свойствам, так и к типам, но в остальном он работает аналогичным образом. Свойство OliveOil имеет тип OliveOil, являющийся, в свою очередь, контрактом, который вы собираетесь экспортировать, поэтому в этом случае вы можете использовать свойство [Export], не задавая явно тип.

Подсказка

Тип всегда можно экспортировать путем создания адаптера.

Когда у класса, который вам необходимо скомпоновать, есть зависимости, вам нужно экспортировать их посредством адаптера. Как показывает следующий листинг, этот процесс становится более сложным, но пока что вполне управляемым.

Листинг 15-2: Настройка класса, имеющего зависимости
public class MayonnaiseAdapter
{
	private readonly Mayonnaise mayo;
	[ImportingConstructor]
	public MayonnaiseAdapter(
		EggYolk yolk, OliveOil oil)
	{
		if (yolk == null)
		{
			throw new ArgumentNullException("yolk");
		}
		if (oil == null)
		{
			throw new ArgumentNullException("oil");
		}
		this.mayo = new Mayonnaise(yolk, oil);
	}
	[Export]
	public Mayonnaise Mayonnaise
	{
		get { return this.mayo; }
	}
}

Строка 5-6: Имитатор сигнатуры конструктора Mayonnaise

Строка 16: Создание Mayonnaise

Строка 18: Экспорт Mayonnaise

Чтобы экспортировать класс Mayonnaise посредством адаптера, вы должны учитывать тот факт, что у него есть зависимости, которые вам нужно будет импортировать. Чтобы иметь возможность создать экземпляр класса, вы должны сымитировать сигнатуру конструктора Mayonnaise в конструкторе адаптера для того, чтобы вы смогли импортировать все необходимые части. После прохождения соответствующих граничных операторов вы создаете новый экземпляр Mayonnaise из параметров конструктора и сохраняете результат в приватном поле. Это и есть паттерн Constructor Injection в действии.

Чтобы экспортировать класс Mayonnaise, вы можете раскрыть поле mayo как свойство и отметить его атрибутом [Export].

Благодаря EggYolkAdapter, похожему на OliveOilAdapter из листинга 15-1, вы можете создать каталог, состоящий из трех адаптеров, и успешно разрешить экземпляр Mayonnaise, даже если вы никогда не изменяли первоначальные классы.

Возможно, вы обратили внимание на атрибут [ImportingConstructor], который появился в листинге 15-2. Это часть другой стороны уравнения. До настоящего момента мы рассматривали процесс экспорта частей. Теперь давайте изучим, как можно импортировать части.

Импорт частей

В рамках MEF присутствует некоторого рода симметрия. Большинство из тех утверждений, которые мы можем применить к атрибутам экспорта, также применимы к атрибутам импорта. Однако когда дело доходит до паттерна Constructor Injection, нам необходимо прибегнуть к атрибуту [ImportingConstructor], эквивалентов которого для экспортируемых компонентов не существует. Мы видели, как этот атрибут применялся к MayonnaiseAdapter в листинге 15-2, но он должен применяться всякий раз, когда нам нужно применять паттерн Constructor Injection.

В приведенном примере мы предположили, что класс Mayonnaise нами не контролируется. Благодаря невероятному стечению обстоятельств мы смогли невзначай перехватить исходный код и теперь можем изменять типы напрямую. В этом случае нам не придется создавать адаптеры, и мы можем применять атрибуты [Export] напрямую к классам Mayonnaise, OliveOil и EggYolk.

MEF не распознал паттерн Constructor Injection, поэтому, даже если у Mayonnaise имеется всего один конструктор, при попытке разрешить этот класс мы сначала получим исключение. Нам необходимо явным образом сообщить MEF о том, какой конструктор он должен использовать при отсутствии конструктора по умолчанию:

[ImportingConstructor]
public Mayonnaise(EggYolk eggYolk, OliveOil oil)

[ImportingConstructor] – это сигнал MEF о том, что тот конструктор, к которому относится этот атрибут, должен использоваться для компоновки типа.

Подсказка

Для конструкторов по умолчанию [ImportingConstructor] не нужен. Используйте этот атрибут, если у класса нет конструктора по умолчанию, или если компоновка осуществляется с помощью другого конструктора, а не конструктора по умолчанию.

Кроме того, мы можем использовать атрибут [Import] для поддержки паттерна Property Injection, но к этому вопросу мы вернемся в разделе 15.4.3, который посвящен этому паттерну. Более того, существует атрибут [ImportMany]. Который используется для импорта последовательностей частей, но его мы рассмотрим в разделе 15.3.2.

Импорт и экспорт частей основывается на применении атрибутов, а поскольку атрибуты компилируются в типы, это делает MEF негибким. Свою гибкость MEF приобретает благодаря каталогам.

Работа с каталогами

Каталог инкапсулирует набор частей, которые контейнер может использовать для компоновки диаграммы объектов. В этом разделе мы приведем обзор различных видов каталогов, доступных в MEF.

Использование каталогов в рамках контейнеров

В разделе 15.1.1 "Разрешение объектов" вы уже видели пример взаимодействия каталога и контейнера:

var catalog = new
	TypeCatalog(typeof(SauceBéarnaise));
var container = new
	CompositionContainer(catalog);

В этом примере используется TypeCatalog конкретного типа, но вы можете создать CompositionContainer с любым ComposablePartCatalog.TypeCatalog – это всего лишь один из множества дочерних классов. На рисунке 15-4 приведена схема иерархии типов.

Рисунок 15-4: В MEF входит четыре конкретных каталога, но помимо них мы можем определять пользовательские каталоги. Возможно, было бы достаточно просто реализовать каталог, который выступал бы в роли Decorator для других каталогов (например, каталог фильтрации), между тем, как настоящий пользовательский каталог был бы в него вложен.

Определение

Каталог – это любой класс, унаследованный от абстрактного класса ComposablePartCatalog.

Как и подразумевает его имя, ComposablePartCatalog – это каталог частей, которые CompositionContainer использует для сопоставления импортируемых и экспортируемых компонентов. Одна из перегрузок конструктора класса CompositionContainer позволяет передавать ComposablePartCatalog, и именно этот конструктор мы использовали до настоящего момента:

public CompositionContainer(ComposablePartCatalog catalog,
	params ExportProvider[] providers)

Помимо того, что этот конструктор принимает в качестве параметра экземпляр ComposablePartCatalog, он также принимает и массив params типа ExportProviders, который является еще одним механизмом расширяемости, не рассматриваемым в этой книге.

Поскольку ComposablePartCatalog – это абстрактный класс, а CompositionContainer принимает в качестве параметра любой унаследованный класс, теоретически мы можем создать пользовательские каталоги с самых азов. Это главный Seam контейнера MEF. Его даже можно использовать для определения других вариантов атрибутивной модели MEF по умолчанию, которая используется для определения импортируемых и экспортируемых компонентов. Несмотря на то, что такой подход возможен, он слишком трудозатратный, поэтому в этой главе мы не будем его рассматривать.

Подсказка

В проекте с открытым исходным кодом MEF Contrib содержится пример пользовательского ComposablePartCatalog, полностью замещающего атрибутивную модель конфигурирования более открытой моделью, которая больше похожа на другие DI-контейнеры.

Все каталоги, входящие в состав MEF в .NET 4, используют атрибуты [Import] и [Export] для определения импортируемых и экспортируемых данных, но по-разному определяют местоположение частей. Например, TypeCatalog определяет местоположение частей, считывая атрибуты типов, содержащихся в каталоге.

Использование каталогов типов

Класс TypeCatalog позволяет определять каталог, состоящий из перечня типов, на основании предположения о том, что эти типы определяют импортируемые и экспортируемые компоненты с помощью атрибутов. Существует два перегруженных конструктора, которые позволяют нам создавать произвольное количество экземпляров Type:

public TypeCatalog(params Type[] types)
public TypeCatalog(IEnumerable<Type> types)

В качестве примера для того, чтобы иметь возможность компоновать Mayonnaise из адаптеров, созданных вами в листингах 15-1 и 15-2, вы можете создать каталог следующим образом:

var catalog = new TypeCatalog(
	typeof(MayonnaiseAdapter),
	typeof(EggYolkAdapter),
	typeof(OliveOilAdapter));

Это небольшой каталог, позволяющий разрешать Mayonnaise. Если вы удалите любой из трех типов адаптеров, то экспортируемые компоненты будут упущены. Помимо того, что этот каталог позволяет вам разрешать Mayonnaise, он также позволяет разрешать EggYolk и OliveOil, но не более того.

Очевидно, что вы могли бы передать в TypeCatalog большее количество типов, чтобы экспортировать больше данных, но при этом вы должны четко определить список типов. Это имеет смысл для небольших сценариев с ограниченной областью применения. Преимущество заключается в том, что вы можете отбирать только те типы, которые вы собираетесь использовать при компоновке. При наличии типов, экспортирующих конкурирующие типы, вы можете отобрать только те типы, которые вам нужны.

Подсказка

Вы можете создать несколько адаптеров, которые совершенно по-разному экспортируют одну и ту же часть, и передать в TypeCatalog только один из них. При написании шаблонного кода для этой главы я использовал этот прием для того, чтобы варьировать атрибуты MayonnaiseAdapter, не редактируя при этом код.

Недостаток применения TypeCatalog заключается в том, что вы должны явно передавать все типы. При добавлении в сборку нового типа вам также нужно добавить его в TypeCatalog, если вы хотите, чтобы он вошел в каталог. Это приводит к нарушению принципа DRY (Don't Repeat Yourself – Не повторяйся). Вы могли бы решить эту проблему путем написания кода, в котором для просмотра сборки с целью обнаружения всех открытых типов используется рефлексия. Но вам не придется этого делать, поскольку уже существует каталог, выполняющий то же самое.

Использование каталогов сборок

Класс AssemblyCatalog предназначен для просмотра сборки с целью поиска всех импортируемых и экспортируемых компонентов, определенных в этой сборке. Этот класс позволяет нам продолжать добавлять части в сборку без необходимости помнить о том, что нужно добавить эту часть еще и в каталог.

Использовать AssemblyCatalog так же просто, как и создавать экземпляр Assembly с помощью конструктора:

var assembly = typeof(Steak).Assembly;
var catalog = new AssemblyCatalog(assembly);

В этом примере вы используете неизбирательный представительский тип (Steak) для определения сборки, но подойдет и любой другой метод, который создает соответствующий экземпляр Assembly.

Кроме того, существует перегрузка конструктора, которая вместо экземпляра Assembly принимает в качестве параметра имя файла. Это делает возможными более слабо связанные сценарии, поскольку мы можем заменить .dll файл, не компилируя повторно остальную часть приложения. Это приближает нас к смыслу подключения сценариев расширений в контейнере MEF. Благодаря AssemblyCatalog мы могли бы написать императивный цикл и создать для каждого найденного нами файла каталог по этому пути. Однако нам не приходится действовать подобным образом, поскольку в MEF уже есть специально предназначенный для этого каталог.

Использование каталогов директорий

Основная цель MEF – разрешить использовать сценарии расширений. Универсальная архитектура расширений – определить для расширений особую директорию. Главное приложение будет загружать и использовать любую сборку, размещенную в этой директории.

MEF способствует выполнению такого сценария посредством класса DirectoryCatalog. В конструктор мы передаем путь к директории, а он, в свою очередь, ищет в этой директории файлы с расширением .dll и загружает все, что обнаружено в этих сборках:

var catalog = new DirectoryCatalog(directory);

Еще одна перегрузка конструктора также позволяет нам задавать шаблон поиска посредством универсальных специальных символов.

Примечание

При использовании MEF в качестве фреймворка расширяемости, что является ключевым его предназначением, предполагается, что DirectoryCatalog является самым универсальным из используемых каталогов.

Несмотря на то, что в директории мы можем разместить произвольное количество сборок, а затем отбирать их с помощью DirectoryCatalog, мы можем захотеть сочетать каталоги из нескольких отличных друг от друга источников. Даже если для включения возможности расширяемости мы используем DirectoryCatalog, нам также могут понадобиться некоторые реализации по умолчанию или внутренние реализации соответствующих импортируемых и экспортируемых элементов. Эти реализации не должны размещаться в папке расширений, потому что в этом случае пользователи смогут удалить из приложения жизненно важную функциональность. Возможно, было бы лучше создать такие реализации по умолчанию с помощью TypeCatalog, но это означает, что нам пришлось бы соединить разные каталоги в один.

Использование каталогов агрегатов

Для соединения каталогов можно использовать класс AggregateCatalog, который является тем же самым Composite только с другим именем. Он объединяет в единое целое произвольное количество каталогов и в то же самое время сам является каталогом:

var catalog = new AggregateCatalog(catalog1, catalog2);
var container = new CompositionContainer(catalog);

Входящие в MEF четыре каталога уже обеспечивают достаточную гибкость приложения, но для получения наибольшего контроля мы реализуем также пользовательские каталоги. Один из примеров, который легко реализовать и использовать, – это каталог фильтрации.

Реализация каталога фильтрации

Несмотря на то, что реализация пользовательского каталога с нуля, возможно, является довольно замысловатой затеей, мы можем довольно легко реализовать Decorator, который изменит поведение другого каталога.

Наиболее очевидный пример – каталог фильтрации, который отфильтровывает вложенный каталог. В следующем листинге продемонстрирован пользовательский каталог, который является оберткой для другого каталога и может вступить в игру только с помощью тех составляющих, которые экспортируют контракт, содержащий строку "Sauce". Этот пользовательский каталог можно использовать, чтобы отобрать из каталога всех ингредиентов только соусы.

Листинг 15-3: Реализация пользовательского каталога
public class SauceCatalog : ComposablePartCatalog
{
	private readonly ComposablePartCatalog catalog;
	public SauceCatalog(ComposablePartCatalog cat)
	{
		if (cat == null)
		{
			throw new ArgumentNullException("cat");
		}
		this.catalog = cat;
	}
	public override
		IQueryable<ComposablePartDefinition> Parts
	{
		get
		{
			return this.catalog.Parts.Where(def =>
				def.ExportDefinitions.Any(x =>
					x.ContractName
						.Contains("Sauce")));
		}
	}
}

Строка 1: Наследование от ComposablePartCatalog

Строка 3-11: Constructor Injection

Строка 12-22: Реализация фильтра

Чтобы реализовать пользовательский каталог, вы выполняете наследование от абстрактного класса ComposablePartCatalog. Поскольку вы пожелали создать обертку для другого каталога, вы запрашиваете этот каталог через Constructor Injection.

Свойство Parts – единственный абстрактный член ComposablePartCatalog, поэтому это и есть тот единственный член класса, который необходимо реализовать. При желании вы можете реализовать и другие виртуальные члены, но для этого примера это не потребуется. Фильтр реализуется с помощью выражения Where, которое отфильтровывает все ComposablePartDefinitions, не экспортирующие никакого контракта и содержащие слово "Sauce".

SauceCatalog – это конкретный класс, но вы можете обобщить реализацию, чтобы создать универсальный FilteringCatalog. В документации к MEF есть соответствующий пример.

Пользовательские каталоги

Возможно, листинг 15-3 вас удивил: если нам для создания пользовательского каталога необходимо реализовать только одно единственное свойство, то, как этот процесс может быть сложным? Проблема заключается в том, что ComposablePartDefinition является абстрактным типом и не имеет ни одной открытой реализации. При реализации унаследованного ComposablePartCatalog необходимо также реализовать пользовательский ComposablePartDefinition. Теперь паттерн повторяется, поскольку ComposablePartDefinition определяет еще один абстрактный метод, возвращаемое значение которого относится к типу, не имеющему открытой реализации. Хотя создать пользовательский каталог возможно, но в этой книге этот процесс не рассматривается.

Каталоги – существенные строительные блоки MEF. Несмотря на то, что атрибуты статичны, каталоги придают приложению большую гибкость. В MEF встроены четыре каталога, которые содержат элементы, полученные из явных типов, из одной и той же сборки или из найденных в папке сборок. В случае необходимости сочетать части из разных каталогов можно воспользоваться AggregateCatalog.

MEF поддерживает только возможность конфигурирования частей с помощью атрибутов. Но если нам нужно скомпоновать части, не используя атрибуты, мы всегда можем создать адаптеры, которые импортируют и экспортируют эти части. Такие адаптеры можно использовать для того, чтобы преодолеть разрыв между статической аттрибутивной моделью и конфигурацией контейнера, которую мы использовали для других DI-контейнеров. Мы можем разделить существующий каталог типов на подклассы с тем, чтобы расфасовать совокупность частей или адаптеров, которые впоследствии можно сочетать с AggregateCatalog для компоновки приложения.

До настоящего момента мы рассматривали только то, как можно определить импортируемые и экспортируемые компоненты таким образом, что MEF мог компоновать диаграмму объектов. Существуют и другие стороны механизма внедрения зависимостей, которые мы еще не рассматривали. Один из наиболее важных вопросов – как управлять жизненным циклом объектов.