Главная страница   /   15.3. Работа с составными компонентами (Внедрение зависимостей в .NET

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

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

Марк Симан

15.3. Работа с составными компонентами

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

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

  • Для разных потребителей должны использоваться разные специфичные типы
  • Зависимости являются последовательностями
  • Используются Decorator'ы

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

Примечание

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

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

Выбор среди составных кандидатов

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

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

Работа с составными реализациями одного и того же экспортируемого компонента

Как вы уже видели в разделе 15.1.2 "Определение экспортируемых и импортируемых элементов", можно создать составные части одного и того же экспорта:

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

В этом примере и SauceBéarnaise, и Steak определяются как экспортируемые компоненты типа IIngredient. Однако в отличие от большинства остальных DI-контейнеров в MEF не используется понятие компонента по умолчанию. Существует либо единственный экспортируемый компонент части, либо несколько компонентов. Это и есть та мощность, которая проиллюстрирована в таблице 15-2. Если вы экспортируете и SauceBéarnaise, и Steak как IIngredient, то получаете составные экспортируемые компоненты, а разрешить их вы можете только посредством импорта составных экземпляров.

При наличии двух экспортируемых компонентов попытка разрешить один экземпляр IIngredient приводит к исключению:

var ingredient = container.GetExportedValue<IIngredient>();

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

Один из способов сделать экспортируемый компонент более индивидуальным – присвоить ему имя. Перегрузка конструктора атрибута [Export] позволяет присвоить экспортируемому компоненту имя:

[Export("sauce", typeof(IIngredient))]
public class SauceBéarnaise : IIngredient { }
[Export("meat", typeof(IIngredient))]
public class Steak : IIngredient { }

В этом примере вместо двух частей, порождающих один и тот же экспортируемый компонент (IIngredient), определяются два разных экспортируемых компонента: один из которых экспортирует сочетание IIngredient и имени "sauce", а другой – сочетание IIngredient и имени "meat". Теперь нет ни одного неименованного контракта IIngredient.

Примечание

При экспорте только именованных типов отсутствуют экспортируемые компоненты неименованного типа.

При попытке разрешить неименованный IIngredient будет выдаваться исключение:

var ingredient = container.GetExportedValue<IIngredient>();

Несмотря на то, что этот код приводит к выдаче исключения, оба именованных экспортируемых компонента можно разрешить, выполнив запрос конкретно этих компонентов:

var meat = container.GetExportedValue<IIngredient>("meat");
var sauce = container.GetExportedValue<IIngredient>("sauce");

Явное разрешение именованного экспортируемого компонента с помощью соответствующей перегрузки GetExportedValue – хороший способ продемонстрировать, как разрешаются части, но если мы руководствуемся паттерном Register Release Resolve, то нет необходимости запрашивать таким способом конкретный именованный компонент.

Подсказка

Если вы обнаружите, что вызываете метод GetExportedValue с конкретным именем или идентификатором, подумайте над тем, сможете ли вы сменить свой подход на менее неопределенный.

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

Импорт именованных экспортируемых компонентов

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

public ThreeCourseMeal(ICourse entrée,
	ICourse mainCourse, ICourse dessert)

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

Листинг 15-4: Определение именованных экспортируемых компонентов
[Export("entrée", typeof(ICourse))]
public class Rillettes : ICourse { }
[Export("mainCourse", typeof(ICourse))]
public class CordonBleu : ICourse { }
[Export("dessert", typeof(ICourse))]
public class MousseAuChocolat : ICourse { }

Вы помечаете Rilettes экспортируемым компонентом под названием "entrée", класс CordonBleu – экспортируемым компонентом под названием "mainCourse", а MousseAuChocolat – экспортируемым компонентом под названием "dessert".

Принимая во внимание эти экспортируемые компоненты, теперь вы можете следующим образом отметить конструктор класса ThreeCourseMeal соответствующими атрибутами [Import]:

[ImportingConstructor]
public ThreeCourseMeal(
	[Import("entrée", typeof(ICourse))]ICourse entrée,
	[Import("mainCourse", typeof(ICourse))]ICourse mainCourse,
	[Import("dessert", typeof(ICourse))]ICourse dessert)

Обратите внимание на то, что вы можете применить атрибут [Import] к аргументам конструктора. Обычно в тех случаях, когда конструктор уже отмечен атрибутом [ImportingConstructor], вам не нужно явным образом применять атрибут [Import] к аргументам конструктора. Но в этом примере вам необходимо отметить каждый параметр атрибутом, чтобы они соответствовали разным именованным экспортируемым компонентам. Поскольку в листинге 15-4 присутствуют соответствующие экспортируемые компоненты, из этих частей вы теперь можете успешно разрешить класс ThreeCourseMeal.

Подсказка

Если вы не можете (или не хотите) явным образом изменять классы, можете вместо них создать экспортирующие адаптеры.

Метаданные

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

Еще один вариант – использовать такую возможность MEF, как метаданные. Эта возможность позволяет определять пользовательские атрибуты экспорта, инкапсулирующие дополнительные метаданные, которые мы собираемся добавить в экспортируемый компонент. Полноценная трактовка метаданных не будет рассматриваться в этой книге, но если вы хотите узнать о них больше, обратитесь к статье Глена Блока "Managed Extensibility Framework" в журанле "MSDN Magazine".

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

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

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

В разделе 10.3.2 "Разработка пользовательского стиля существования" мы обсуждали, как выполнить рефакторинг явного класса ThreeCourseMeal к более универсальному классу Meal, который обладает приведенным ниже конструктором:

public Meal(IEnumerable<ICourse> courses)

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

Автоматическая интеграция последовательностей

Как мы уже говорили в разделах 15.1.2 "Определение экспортируемых и импортируемых элементов" и 15.3.1 "Выбор среди составных кандидатов", в MEF используется такое понятие, как мощность. Это также означает, что MEF разбирается в составных импортируемых и экспортируемых компонентах, но чтобы мы должны их явно определять. В разделе 15.1.2 вы видели, как нужно применять атрибут [ImportingConstructor], чтобы обеспечить возможность использования паттерна Constructor Injection. Несмотря на то, что нужно применять к конструктору Meal атрибут [ImportingConstructor], этого недостаточно. Такое поведение указывает MEF на то, что конструктор Meal необходимо использовать для композиции, но при этом предполагается использовать импортируемый компонент IEnumerable<ICourse>.

Экспортировать части ICourse можно так, как это продемонстрировано в листинге 15-4. Однако поскольку теперь вы не хотите явно различать эти части, ни одной из них не присваивается имя:

[Export(typeof(ICourse))]
public class Rillettes : ICourse { }
[Export(typeof(ICourse))]
public class CordonBleu : ICourse { }
[Export(typeof(ICourse))]
public class MousseAuChocolat : ICourse { }

Обратите внимание, что единственное отличие от листинга 15-4 заключается в том, что ни один из экспортируемых компонентов не имеет имени. Сейчас у вас имеются составные экспортируемые компоненты типа ICourse, но это, само по себе, еще не устраняет несоответствия между составными экспортируемыми компонентами ICourse и единственным импортируемым компонентом IEnumerable<ICourse>. Последним шагом будет применение атрибута [ImportMany]:

[ImportingConstructor]
public Meal([ImportMany]IEnumerable<ICourse> courses)

Атрибут [ImportMany] используется для того, чтобы явно преобразовать составные экспортируемые компоненты в единственный оператор импорта последовательностей. Экспортируемые компоненты могут браться из разных сборок, но при этом они будут компоноваться в одну последовательность. При разрешении IMeal вы получаете экземпляр Meal, имеющий три экспортируемых компонента ICourse: Rillettes, CordonBleu и MousseAuChocolat.

С помощью атрибута [ImportMany] часть может импортировать последовательность всех соответствующих экспортируемых компонентов. Только когда нам нужно из большого набора экземпляров явно отобрать только несколько из них, нам необходимо выполнить больше действий. Рассмотрим, как это сделать.

Отбор нескольких экспортируемых компонентов из большого набора

Когда мы имеем дело с множественностью экспортируемых компонентов, та стратегия, которая подразумевается под использованием атрибута [ImportMany], является корректной линией поведения. Это позволяет сопоставить импортеру все экспортируемые компоненты необходимого контракта. Но, как показывает рисунок 15-5, возможны случаи, когда из большого набора всех экспортируемых компонентов нам необходимо отобрать только некоторые из них.

Рисунок 15-5: В ситуации, продемонстрированной слева, мы хотим явным образом отобрать определенные зависимости из большого списка всех экспортируемых компонентов. Это отличается от ситуации, приведенной справа, когда мы отбираем все без разбора.

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

Единственный способ это сделать – еще раз прибегнуть к именованным экспортируемым компонентам. Однако выбранная нами стратегия немного отличается от используемой в листинге 15-4, поскольку теперь для того, чтобы отметить все те экспортируемые компоненты, которые необходимо импортировать в класс Meal, мы собираемся использовать именованный атрибут экспорта. Как показано в следующем листинге, это не исключает экспорта других контрактов, а также контракта, лежащего в основе совокупности компонентов.

Листинг 15-5: Отбор экспортируемых компонентов из набора
[Export(typeof(ICourse))]
[Export("meal", typeof(ICourse))]
public class Rillettes : ICourse { }
[Export(typeof(ICourse))]
public class LobsterBisque { }
[Export(typeof(ICourse))]
[Export("meal", typeof(ICourse))]
public class CordonBleu : ICourse { }
[Export(typeof(ICourse))]
[Export("meal", typeof(ICourse))]
public class MousseAuChocolat : ICourse { }

Строка 2, 7, 10: Искомые экспортируемые компоненты

Строка 1, 6, 9: Обычные атрибуты экспорта

Строка 4-5: Не выполняется отбор компонентов

Все три класса Rillettes, CordonBleu и MousseAuChocolat экспортируют контракт с именем "meal". Этот именованный контракт можно использовать для импорта только тех частей, которые экспортируют этот конкретный контракт. Однако для других потребителей, которым могут понадобиться все экспортируемые компоненты ICourse независимо от их имени, можно также экспортировать эти три класса как неименованный контракт ICourse. К части можно добавлять сколько угодно атрибутов [Export].

Класс LobsterBisque экспортирует не именованный контракт meal, а только неименованный контракт ICourse. Это означает, что те потребители, которые собираются импортировать все экспортируемые компоненты ICourse, могут сделать это с помощью атрибута [ImportMany] по умолчанию. Однако вы все равно можете установить, что часть импортирует только те части, которые явно экспортируют именованный контракт meal:

[ImportingConstructor]
public Meal(
	[ImportMany("meal", typeof(ICourse))]
	IEnumerable<ICourse> courses)

Вместо конструктора по умолчанию атрибута [ImportMany] можно использовать перегрузку конструктора, которая позволяет импортировать только именованный контракт. Атрибут помечает параметр courses, что означает, что в последовательность courses будут отбираться только те части, которые экспортируют именованный контракт meal. Благодаря экспортируемым компонентам из листинга 15-5 вы получите Meal, содержащий Rillettes, CordonBleu и MousseAuChocolat, но без LobsterBisque.

Именованные экспортируемые компоненты можно использовать в качестве маркеров таким образом, чтобы помеченные экспортируемые компоненты можно было выборочно отбирать в потребители. Поскольку можно использовать сколько угодно атрибутов [Export], помечать экспортируемые компоненты можно для разных целей.

В обоих случаях, продемонстрированных на рисунке 15-5, атрибут [ImportMany] является ключевым элементом, позволяющим импортировать множественные экспортируемые компоненты в одного потребителя. Импорт последовательностей – хороший способ избавиться от неопределенности, а используемые в MEF понятие мощности и явные атрибуты делают этот процесс более понятным.

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

Интеграция Decorator'ов

В разделе 9.1.2 "Паттерны и принципы механизма перехвата" мы обсуждали то, насколько паттерн проектирования Decorator полезен при реализации сквозных сущностей. По определению Decorator'ы представляют собой составные типы одной и той же абстракции. У нас есть, по крайней мере, две реализации абстракции: сам Decorator и вложенный в него тип. Если бы мы помещали Decorator'ы в стек, то у нас было бы еще больше реализаций.

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

Создание обертки с помощью конкретных контрактов

Рассмотрим проверенный класс Breading, который является Decorator'ом для IIngredient. Для получения экземпляра, оберткой для которого он должен стать, класс Breading использует паттерн Constructor Injection:

public Breading(IIngredient ingredient)

Чтобы получить Cotoletta, вам необходимо будет вложить VealCutlet (еще один IIngredient) в класс Breading. Один из способов это сделать – связать VealCutlet с классом Breading, используя конкретный класс VealCutlet в качестве контракта:

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

Обратите внимание, что часть VealCutlet экспортирует не IIngredient, а только конкретный тип, даже если она реализует интерфейс. Теперь конструктор Breading может явно утверждать, что он импортирует конкретный контракт VealCutlet:

[ImportingConstructor]
public Breading(
	[Import(typeof(VealCutlet))]
	IIngredient ingredient)

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

Примечание

Этот подход концептуально схож с подходом, описанным в разделе 14.3.3, где мы использовали Unity для компоновки Breading и VealCutlet через класс VealCutlet.

Примечание

Поскольку атрибут компилируется в класс, будет проще изменить конструктор Breading таким образом, чтобы он принимал в качестве параметра VealCutlet, а не IIngredient. По-моему, это хорошая демонстрация недостатков использования атрибутов для управления компоновкой.

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

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

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

Части Breading и VealCutlet компонуются вместе, поскольку обладают сопоставляемыми контрактами. Точная форма контракта не так важна. В этом примере вы использовали конкретный тип вложенного класса, но могли бы использовать именованный контракт или, как в этом конкретном случае, любую явную строку. Важно, что соответствие между двумя частями является определенным.

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

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