Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
компонент по умолчанию. Все экспортируемые компоненты равны между собой. Это и есть первый намек на такое важное понятие MEF, как мощ ност ь импортируемых и экспортируемых компонентов. Количество экспортируемых компонентов должно совпадать с количеством импортируемых компонентов. В таблице 15-2 показано, как MEF сопоставляет импортируемые и экспортируемые компоненты по мощности. Таблица 15-2: Сопоставление количества атрибутов импорта и экспорта Export.Single Export.Many Import.Single Равно Не равно Import.Many Равно Равно В этом контексте термин "many" используется для обозначения последовательности частей, обычно массива IEnumerable . Если мы будем явным образом импортировать множество частей одного и того же контракта, то MEF всегда будет находить соответствие, поскольку нулевое количество атрибутов экспорта есть особый случай множественных атрибутов экспорта. С другой стороны, явно импортируя один экземпляр, мы получаем несоответствие мощностей в случае наличия нулевого или множественного количества атрибутов экспорта, потому что импорт одного экземпляра означает, что у нас должен быть только один экземпляр. П римечание Мощ ность – это один из тех видов размерностей, по которым атрибуты экспорта и импорта должны соответствовать друг другу. Как вы увидите в разделе 15.2.1, механизм управления жизненным циклом также может играть важную роль в вопросе соответствия частей, но мощность учитывается всегда. Далее в этом разделе вы увидите, как определять единичные и множественные атрибуты импорта. Но перед тем как приступить к рассмотрению этого вопроса, нам следует 595 рассмотреть процесс экспорта частей в тех ситуациях, когда мы не контролируем классы, участвующие в этом процессе. Экспорт адапте ров Самый простой способ экспортировать часть – применить к классу атрибут [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 через адаптер 1. public class OliveOilAdapter 2. { 3. private readonly OliveOil oil; 4. public OliveOilAdapter() 5. { 6. this.oil = new OliveOil(); 7. } 8. [Export] 9. public OliveOil OliveOil 10. { 11. get { return this.oil; } 12. } 13. } Строка 8: Экспорт свойства Класс OliveOilAdapter – это совершенно новый класс, который обертывает первоначальный класс OliveOil и экспортирует его посредством аннотированного 596 свойства. Атрибут [Export] можно применять как к свойствам, так и к типам, но в остальном он работает аналогичным образом. Свойство OliveOil имеет тип OliveOil , являющийся, в свою очередь, контрактом, который вы собираетесь экспортировать, поэтому в этом случае вы можете использовать свойство [Export] , не задавая явно тип. П одсказка Т ип всегда можно экспортировать путем создания адаптера. Когда у класса, который вам необходимо скомпоновать, есть зависимости, вам нужно экспортировать их посредством адаптера. Как показывает следующий листинг, этот процесс становится более сложным, но пока что вполне управляемым. Листинг 15-2: Настройка класса, имеющего зависимости 1. public class MayonnaiseAdapter 2. { 3. private readonly Mayonnaise mayo; 4. [ImportingConstructor] 5. public MayonnaiseAdapter( 6. EggYolk yolk, OliveOil oil) 7. { 8. if (yolk == null) 9. { 10. throw new ArgumentNullException("yolk"); 11. } 12. if (oil == null) 13. { 14. throw new ArgumentNullException("oil"); 15. } 16. this.mayo = new Mayonnaise(yolk, oil); 17. } 18. [Export] 19. public Mayonnaise Mayonnaise 20. { 21. get { return this.mayo; } 22. } 23. } Строка 5-6: Имитатор сигнатуры конструктора Mayonnaise Строка 16: Создание Mayonnaise Строка 18: Экспорт Mayonnaise Чтобы экспортировать класс Mayonnaise посредством адаптера, вы должны учитывать тот факт, что у него есть зависимости, которые вам нужно будет импортировать. Чтобы иметь возможность создать экземпляр класса, вы должны сымитировать сигнатуру конструктора Mayonnaise в конструкторе адаптера для того, чтобы вы смогли импортировать все необходимые части. После прохождения соответствующих граничных операторов вы создаете новый экземпляр Mayonnaise из параметров конструктора и сохраняете результат в приватном поле. Это и есть паттерн Constructor Injection в действии. Чтобы экспортировать класс Mayonnaise , вы можете раскрыть поле mayo как свойство и отметить его атрибутом [Export] 597 Благодаря 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 приобретает благодаря каталогам. 598 Работа с каталогами Каталог инкапсулирует набор частей, которые контейнер может использовать для компоновки диаграммы объектов. В этом разделе мы приведем обзор различных видов каталогов, доступных в 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 для других каталогов (например, каталог фильтрации), между тем, как настоящий пользовательский каталог был бы в него вложен. 599 О пределение Кат алог – это любой класс, унаследованный от абстрактного класса ComposablePartCatalog Как и подразумевает его имя, ComposablePartCatalog – это каталог частей, которые CompositionContainer использует для сопоставления импортируемых и экспортируемых компонентов. Одна из перегрузок конструктора класса CompositionContainer позволяет передавать ComposablePartCatalog , и именно этот конструктор мы использовали до настоящего момента: public CompositionContainer(ComposablePartCatalog catalog, params ExportProvider[] providers) Помимо того, что этот конструктор принимает в качестве параметра экземпляр ComposablePartCatalog , он также принимает и массив param s типа 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 600 В качестве примера для того, чтобы иметь возможность компоновать 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 601 Кроме того, существует перегрузка конструктора, которая вместо экземпляра 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); 602 Входящие в MEF четыре каталога уже обеспечивают достаточную гибкость приложения, но для получения наибольшего контроля мы реализуем также пользовательские каталоги. Один из примеров, который легко реализовать и использовать, – это каталог фильтрации. Ре ализация каталога фильтрации Несмотря на то, что реализация пользовательского каталога с нуля, возможно, является довольно замысловатой затеей, мы можем довольно легко реализовать Decorator, который изменит поведение другого каталога. Наиболее очевидный пример – каталог фильтрации, который отфильтровывает вложенный каталог. В следующем листинге продемонстрирован пользовательский каталог, который является оберткой для другого каталога и может вступить в игру только с помощью тех составляющих, которые экспортируют контракт, содержащий строку "Sauce". Этот пользовательский каталог можно использовать, чтобы отобрать из каталога всех ингредиентов только соусы. Листинг 15-3: Реализация пользовательского каталога 1. public class SauceCatalog : ComposablePartCatalog 2. { 3. private readonly ComposablePartCatalog catalog; 4. public SauceCatalog(ComposablePartCatalog cat) 5. { 6. if (cat == null) 7. { 8. throw new ArgumentNullException("cat"); 9. } 10. this.catalog = cat; 11. } 12. public override 13. IQueryable 14. { 15. get 16. { 17. return this.catalog.Parts.Where(def => 18. def.ExportDefinitions.Any(x => 19. x.ContractName 20. .Contains("Sauce"))); 21. } 22. } 23. } Строка 1: Наследование от ComposablePartCatalog Строка 3-11: Constructor Injection Строка 12-22: Реализация фильтра Чтобы реализовать пользовательский каталог, вы выполняете наследование от абстрактного класса ComposablePartCatalog . Поскольку вы пожелали создать обертку для другого каталога, вы запрашиваете этот каталог через Constructor Injection. Свойство Parts – единственный абстрактный член ComposablePartCatalog , поэтому это и есть тот единственный член класса, который |