Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
611 Несмотря на то, что этот код приводит к выдаче исключения, оба именованных экспортируемых компонента можно разрешить, выполнив запрос конкретно этих компонентов: var meat = container.GetExportedValue Явное разрешение именованного экспортируемого компонента с помощью соответствующей перегрузки 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 экспортируемым компонентом под названием "ent rée", класс CordonBleu – экспортируемым компонентом под названием "mainCourse", а MousseAuChocolat – экспортируемым компонентом под названием "dessert ". 612 Принимая во внимание эти экспортируемые компоненты, теперь вы можете следующим образом отметить конструктор класса 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 Ext ensibility Fram ework" в журанле "MSDN Magazine". Чтобы избавиться от неопределенности, мы всегда можем сопоставить именованные импортируемые компоненты со схожими именованными экспортируемыми компонентами, но лучшим решением будет сконструировать вышеупомяну тое API. Это приводит и к лучешму дизайну в целом. В следующем разделе вы увидите, как можно использовать менее неопределенный и более гибкий подход, при котором разрешается использовать в обеде несколько блюд. Т еперь вы должны изучить то, как MEF работает со списками и последовательно ст ями. 613 Интеграция последовательностей В разделе 10.3.2 "Разработка пользовательского стиля существования" мы обсуждали, как выполнить рефакторинг явного класса ThreeCourseMeal к более универсальному классу Meal , который обладает приведенным ниже конструктором: public Meal(IEnumerable В этом разделе мы рассмотрим то, как можно сконфигурировать MEF, чтобы он интегрировал экземпляры Meal с соответствующими зависимостями ICourse . После рассмотрения этого вопроса вы должны будете приобрести хорошее понимание тех возможностей, которые доступны в случае необходимости конфигурирования частей, имеющих последовательности зависимостей. Автоматическая инте грация последовательностей Как мы уже говорили в разделах 15.1.2 "Определение экспортируемых и импортируемых элементов" и 15.3.1 "Выбор среди составных кандидатов", в MEF используется такое понятие, как мощность. Это также означает, что MEF разбирается в составных импортируемых и экспортируемых компонентах, но чтобы мы должны их явно определять. В разделе 15.1.2 вы видели, как нужно применять атрибут [ImportingConstructor] , чтобы обеспечить возможность использования паттерна Constructor Injection. Несмотря на то, что нужно применять к конструктору Meal атрибут [ImportingConstructor] , этого недостаточно. Такое поведение указывает MEF на то, что конструктор Meal необходимо использовать для композиции, но при этом предполагается использовать импортируемый компонент IEnumerable Экспортировать части 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 . Последним шагом будет применение атрибута [ImportMany] : [ImportingConstructor] public Meal([ImportMany]IEnumerable Атрибут [ImportMany] используется для того, чтобы явно преобразовать составные экспортируемые компоненты в единственный оператор импорта последовательностей. Экспортируемые компоненты могут браться из разных сборок, но при этом они будут компоноваться в одну последовательность. При разрешении IMeal вы получаете 614 экземпляр Meal , имеющий три экспортируемых компонента ICourse : Rillettes , CordonBleu и MousseAuChocolat С помощью атрибута [ImportMany] часть может импортировать последовательност ь всех соответствующих экспортируемых компонентов. Только когда нам нужно из большого набора экземпляров явно отобрать только несколько из них, нам необходимо выполнить больше действий. Рассмотрим, как это сделать. О тбор нескольких экспортируе мы х компонентов из большого набора Когда мы имеем дело с множественностью экспортируемых компонентов, та стратегия, которая подразумевается под использованием атрибута [ImportMany] , является корректной линией поведения. Это позволяет сопоставить импортеру все экспортируемые компоненты необходимого контракта. Но, как показывает рисунок 15-5, возможны случаи, когда из большого набора всех экспортируемых компонентов нам необходимо отобрать только некоторые из них. Рисунок 15-5: В ситуации, продемонстрированной слева, мы хотим явным образом отобрать определенные зависимости из большого списка всех экспортируемых компонентов. Это отличается от ситуации, приведенной справа, когда мы отбираем все без разбора. Когда мы ранее позволяли MEF автоматически интегрировать все экспортируемые компоненты, это соответствовало бы ситуации, изображенной в правой части рисунка 15- 5. Если нам нужно скомпоновать часть так, как изображено в левой части рисунка, то мы должны явно определить, какие экспортируемые компоненты необходимо использовать. Единственный способ это сделать – еще раз прибегнуть к именованным экспортируемым компонентам. Однако выбранная нами стратегия немного отличается от используемой в листинге 15-4, поскольку теперь для того, чтобы отметить все те экспортируемые 615 компоненты, которые необходимо импортировать в класс Meal , мы собираемся использовать именованный атрибут экспорта. Как показано в следующем листинге, это не исключает экспорта других контрактов, а также контракта, лежащего в основе совокупности компонентов. Листинг 15-5: Отбор экспортируемых компонентов из набора 1. [Export(typeof(ICourse))] 2. [Export("meal", typeof(ICourse))] 3. public class Rillettes : ICourse { } 4. [Export(typeof(ICourse))] 5. public class LobsterBisque { } 6. [Export(typeof(ICourse))] 7. [Export("meal", typeof(ICourse))] 8. public class CordonBleu : ICourse { } 9. [Export(typeof(ICourse))] 10. [Export("meal", typeof(ICourse))] 11. 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 Вместо конструктора по умолчанию атрибута [ImportMany] можно использовать перегрузку конструктора, которая позволяет импортировать только именованный контракт. Атрибут помечает параметр courses , что означает, что в последовательность courses будут отбираться только те части, которые экспортируют именованный контракт meal . Благодаря экспортируемым компонентам из листинга 15-5 вы получите Meal , содержащий Rillettes , CordonBleu и MousseAuChocolat , но без LobsterBisque Именованные экспортируемые компоненты можно использовать в качестве маркеров таким образом, чтобы помеченные экспортируемые компоненты можно было выборочно отбирать в потребители. Поскольку можно использовать сколько угодно атрибутов [Export] , помечать экспортируемые компоненты можно для разных целей. 616 В обоих случаях, продемонстрированных на рисунке 15-5, атрибут [ImportMany] является ключевым элементом, позволяющим импортировать множественные экспортируемые компоненты в одного потребителя. Импорт последовательностей – хороший способ избавиться от неопределенности, а используемые в MEF понятие мощности и явные атрибуты делают этот процесс более понятным. Потребители, которые полагаются на последовательности зависимостей, могут быть самым интуитивно понятным применением составных экспортируемых компонентов одной и той же абстракции. Но перед тем как мы полностью отойдем от данной темы, нам необходимо рассмотреть последний (и, возможно, слегка неожиданный) случай, когда в дело вступают составные экземпляры. Интеграция Decorator'ов В разделе 9.1.2 "Паттерны и принципы механизма перехвата" мы обсуждали то, насколько паттерн проектирования Decorator полезен при реализации сквозных сущностей. По определению Decorator'ы представляют собой составные типы одной и той же абстракции. У нас есть, по крайней мере, две реализации абстракции: сам Decorat or и вложенный в него тип. Если бы мы помещали Decorator'ы в стек, то у нас было бы еще больше реализаций. Это еще один пример составных экспортируемых компонентов одного и того же контракта. В отличие от предыдущих разделов эти экспортируемые компоненты не являются концептуально равносильными, а зависят друг от друга. В этом разделе я вы увидите, как сконфигурировать части, чтобы они могли использовать этот паттерн. В рамках MEF компоновать Decorat or'ы можно несколькими способами, но поскольку все они похожи друг на друга, мы рассмотрим только один из них. С оздание обертки с помощью конкретных контрактов Рассмотрим проверенный класс 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) 617 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. Поскольку это не всегда происходит именно так, в следующем разделе мы сделаем краткий обзор того, как работать с классами, экземпляры которых должны создаваться особым образом. 618 15.4. Конфигурирование сложных API До настоящего момента мы рассматривали то, как можно компоновать части, использующие Constructor Injection. Одним из главных преимуществ Constructor Injection является то, что DI-контейнеры могут с легкостью понимать, как компоновать и создавать все классы диаграммы зависимостей. В MEF, с другой стороны, необходимо явно использовать атрибут [ImportingConstructor] , поэтому для MEF это не совсем справедливо. В этом разделе вы увидите, как работать с простейшими аргументами конструктора, статическими фабриками и Property Injection. Все это требует особого внимания. Начнем с рассмотрения классов, конструкторы которых принимают в качестве параметров простейшие типы, например, строки и целые числа. Конфигурирование простейших зависимостей Пока мы внедряем абстракции в потребителей, все в порядке. Но данный процесс усложняется, если конструктор зависит от простейшего типа, например, строкового, числового или перечисляемого. Наиболее часто это случается в реализациях доступа к данным, которые принимают в качестве параметра конструктора строку соединения. Но в то же время это является более общей проблемой, касающейся всех строковых и числовых типов. В сущности, регистрация строкового или числового типа в качестве контракта не имеет особого смысла. Что значит импортировать строку или число, если мы руководствуемся только типом? Нужна ли нам любая строка? В основном нам нужна конкретная строка, например, строка соединения. Такие же рассуждения можно применить и к простейшему значению, включая строки, числа и перечисления. Рассмотрим в качестве примера приведенный ниже конструктор: public ChiliConCarne(Spiciness spiciness) В этом примере Spiciness имеет перечисляемый тип: public enum Spiciness { Mild = 0, Medium, Hot } П редупреждение Согласно эмпирическому правилу перечисления являются code smell'ами и их нужно преобразовывать в полиморфные классы (имеющие разное состояние). Тем не менее, для данного примера они вполне нам подходят. Чтобы соответствующим образом пометить ChiliConCarne , вы добавляете к конструктору атрибут [ImportingConstructor] . При экспорте Spiciness лучше всего сделать это с помощью адаптера: 619 public class SpicinessAdapter { [Export] public Spiciness Spiciness { get { return Spiciness.Hot; } } } Этот адаптер экспортирует значение Spiciness.Hot таким образом, что, если вы компонуете ChiliConCarne из каталога, в котором содержатся эти части, то вы получите горячее |