Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
Можно ли сделать это лучшим способом? Наверняка упростить конфигурацию можно, но это приведет к меньшему контролю. Как иллюстрирует рисунок 10-6, иногда нам нужно уметь делать выбор из списка всех сконфигурированных сервисов данного типа, но в других ситуациях нам нужны они все. 398 Рисунок 10-6: Существует несколько способов работы с исчисляемыми зависимостями. В ситуации, продемонстрированно й справа, необходимо разрешить все сервисы, сконфигурированные в контейнере. В ситуации слева – только некоторую подгруппу. Пример, который вы видели, соответствует ситуации слева, при которой мы тщательно подбираем явный список именованных сервисов из концептуально громадного списка всех сконфигурированных сервисов данного типа. В остальных случаях мы, возможно, предпочтем более простое соглашение, согласно которому мы используем все доступные сервисы запрашиваемого типа. Давайте посмотрим, как это может выполняться. Разрешение последовательностей Castle Windsor по умолчанию не разрешает массивы или IEnumerable . Это может показаться довольно неожиданным, поскольку в результате вызова ResolveAll возвращается массив: IIngredient[] ingredients = container.ResolveAll Тем не менее, если вы попытаетесь позволить контейнеру разрешить компонент, который зависит от массива сервисов, вы получите исключение. Подходящий способ решения данной проблемы – зарегистрировать встроенный CollectionResolver с помощью контейнера, как это показано ниже: container.Kernel.Resolver.AddSubResolver( new CollectionResolver(container.Kernel)); CollectionResolver даст контейнеру возможность разрешать такие последовательно сти зависимостей, как IEnumerable . Благодаря этому теперь вы можете разрешать класс Meal , не используя явный ServiceOverrides . Имея данную регистрацию container.Register(Component .For .ImplementedBy 399 вы можете разрешать IMeal с помощью CollectionResolver : var meal = container.Resolve Будет создан экземпляр Meal со всеми сервисами ICourse контейнера. Возможно, потребители, которые полагаются на списки зависимостей, являются самым понятным случаем использования составных регистраций одной и той же абстракции, но перед тем, как окончательно завершить обсуждение этой темы, необходимо рассмотреть последний и, вероятно, слегка неожиданный случай: когда начинают действовать составные регистрации. Интеграция Decorator'ов В разделе 9.1.2 "Паттерны и принципы механизма перехвата" мы обсуждали то, насколько паттерн проектирования Decorator полезен при реализации сквозных сущностей. По определению Decorator'ы представляют собой составные типы одной и той же абстракции. У нас есть, по крайней мере, две реализации абстракции: сам Decorat or и обернутый тип. Если бы мы помещали Decorat or'ы в стек, то у нас было бы еще больше реализаций. Это еще один пример составных регистраций одного и того же сервиса. В отличие от предыдущих разделов эти регистрации не являются концептуально равносильными, а зависят друг от друга. В данном разделе вы увидите, как сконфигурировать Castle Windsor таким образом, чтобы он мог работать с этим паттерном. Явное подключение Decorator'ов Castle W indsor требует от нас регистрации всех компонентов, которые мы собираемся использовать. Когда дело касается Decorator'ов, мы должны регистрировать и Decorator, и обернутые типы. Поскольку оба типа реализуют один и тот же интерфейс, мы, тем самым, вводим неопределенность, которую можем побороть. Как показывает следующий листинг, мы можем сделать это явным образом. Листинг 10-9: Явная конфигурация Decorator'а container.Register(Component .For .ImplementedBy .ServiceOverrides(new { ingredient = "cutlet" })); container.Register(Component .For .ImplementedBy .Named("cutlet")); Как мы уже обсуждали во введении к главе 9, панировку можно рассматривать в качестве обертки для говяжьей отбивной – регистрация Cotoletta . При разрешении Cotoletta вам нужна ссылка на breading , которая должна содержать в себе veal cutlet . Сначала вы регистрируете Breading . Вспомните, что в Castle Windsor всегда выигрывает первая регистрация. Вы явным образом используете метод ServiceOverrides для того, чтобы сконфигурировать, какой именованный сервис должен использоваться для параметра конструктора ingredient . Обратите внимание на то, что вы ссылаетесь на компонент под 400 названием cutlet , несмотря на то, что данный компонент на данном этапе еще не был зарегистрирован. Т акое возможно, поскольку порядок регистрации мало что значит. Можно регистрировать компоненты до того, как вы зарегистрируете их зависимости, и при этом все будет работать, поскольку при попытке разрешения сервисов все регистрируется должным образом. Это означает, что перед разрешением IIngredient вы все равно должны зарегистрировать veal cutlet . Впоследствии вы регистрируете ее под именем cutlet . Это имя совпадает с именем сервиса, переданного в параметр конструктора ingredient в предыдущей конфигурации. Несмотря на то, что такая явная конфигурация Decorat or'ов возможна и иногда даже необходима, Castle Windsor по своей природе понимает паттерн Decorator и предоставляет более неявный способ выполнения тех же самых действий. Н еявное подключение Decorator'ов Castle W indsor дает нам возможность конфигурировать Decorator'ы неявным образом, регистрируя их в подходящем порядке. Вспомните, что выигрывает первая регистрация, в том смысле, что это тип, который возвращается в результате вызова метода Resolve . Мы должны сначала зарегистрировать наиболее удаленный Decorator. По определению Decorator имеет зависимость от другого экземпляра одного и того же типа. Если мы явным образом не определим, какую регистрацию использовать, мы можем ожидать появления циклических ссылок. Т ем не менее, Castle W indsor умнее всего этого. Он выбирает следующу ю регистрацию соответствующего типа. Это означает, что вместо листинга 10-9 вы можете записать container.Register(Component .For .ImplementedBy .For .ImplementedBy Нет необходимости явным образом присваивать компонентам имена или использовать метод ServiceOverrides для конфигурирования зависимостей. При разрешении IIngredient Castle Windsor будет автоматически интегрировать класс Breading со следующим доступным сервисом IIngredient , то есть классом VealCutlet П римечание Следующий логический шаг вперед от Decorator'а – механизм перехвата. Castle W indsor обладает великолепными возможностями для осуществления перехвата. В разделе 9.3.3 "Пример: перехват с помощью W indsor" уже приводился исчерпывающий пример. Поэтому вместо того, чтобы заново повторять его в данном разделе я сошлюсь на раздел 9.3.3. Castle W indsor позволяет нам работать с составными компонентами несколькими различными способами. Мы можем регистрировать компоненты в виде альтернатив друг другу, в виде пиров, которые разрешаются в виде последовательностей, или в виде иерархических Decorator'ов. В большинстве случаев Castle W indsor будет понимать, что 401 ему делать, но если нам нужен более явный контроль, мы всегда можем использовать метод ServiceOverrides , чтобы явно определить, каким образом компонуются сервисы. Кроме того, это может происходить в ситуации, когда нам нужно иметь дело с API, отклоняющимися от Constructor Injection. До настоящего момента вы наблюдали за тем, как конфигурировать компоненты в WindsorContainer , включая то, как определять стили существования, и как работать с составными компонентами. Но до этого момента мы позволяли контейнеру подключать зависимости, явным образом предполагая, что все компоненты используют Constructor Injection. Поскольку это не всегда происходит именно так, в следующем разделе мы сделаем краткий обзор того, как работать с классами, экземпляр ы которых должны создаваться особым образом. 402 10.4. Конфигурирование сложных API До настоящего момента мы рассматривали то, как можно конфигурировать компоненты, использующие Constructor Injection. Одним из главных преимуществ Constructor Injection является то, что DI-контейнеры, например, Castle Windsor, могут с легкостью понимать, как компоновать и создавать все классы диаграммы зависимостей. Все становится менее понятным, когда API не столь хорошо функционируют. В данном разделе вы увидите, как работать с простыми аргументами конструктора, статическими фабриками и Property Injection. Все это требует особого внимания. Давайте начнем с рассмотрения классов, которые принимают в качестве параметров простыми типы, например, строки и целые числа. Конфигурирование простыми зависимостей Пока мы внедряем абстракции в потребителей, все в порядке. Но данный процесс усложняется, если конструктор зависит от простого типа, например, строкового, числового или перечисляемого. Наиболее часто это случается в реализациях доступа к данным, которые принимают в качестве параметра конструктора строку соединения. Но в то же время это является более общей проблемой, касающейся всех строковых и числовых типов. В сущности, регистрация строкового или числового типа в качестве компонента контейнера не имеет особого смысла, а в Castle Windsor это и вовсе не работает. Если мы попытаемся разрешить компонент с простой зависимостью, мы получим исключение, даже если простой тип был до этого зарегистрирован. Рассмотрите в качестве примера приведенный ниже конструктор: public ChiliConCarne(Spiciness spiciness) В этом примере Spiciness имеет перечисляемый тип: public enum Spiciness { Mild = 0, Medium, Hot } П редупреждение Согласно эмпирическому правилу перечисления являются "запахами" и их нужно преобразовать в полиморфные классы (имеющие разное состояние). Тем не менее, для данного примера они вполне нам подходят. Необходимо явным образом сообщить Castle W indsor о том, как разрешать параметр конструктора spiciness . Следующий листинг демонстрирует, как это можно сделать, используя синтаксис, очень похожий на метод ServiceOverrides , но представляющий собой другой метод. 403 Листинг 10-10: Применение простого значения для аргумента конструктора container.Register(Component .For .ImplementedBy .DependsOn(new { spiciness = Spiciness.Hot })); Вместо метода ServiceOverrides , который переопределяет автоматическую интеграцию, можно использовать метод DependsOn , который дает возможность применять экземпляры конкретных зависимостей. В данном примере для параметра конструктора spiciness вы используете значение Spiciness.Hot П римечание Разница между ServiceOverrides и DependsOn заключается в том, что в рамках DependsOn мы применяем фактические экземпляры, которые используются для данного параметра или свойства, тогда как в рамках ServiceOverrides мы используем имена и типы сервисов, которые будут разрешаться для данного параметра или свойства. П редупреждение Как и в случае с ServiceOverrides , метод DependsOn полагается на соответствие между именем параметра и именем безымянного свойства, применяемого в DependsOn . Если мы переименуем параметр, то мы должны отредактировать также и вызов DependsOn Всякий раз, когда нам нужно использовать простое значение, например, строку соединения, мы можем явным образом определить значение в коде (или взять его из конфигурации приложения) и присвоить его при помощи метода DependsOn . Что хорошо при использовании DependsOn , так это то, что нам не нужно явным образом вызывать конструктор или применять какие-либо другие зависимости, в которых автоматическая интеграция была бы более подходящей. Но недостаток использования DependsOn – он более хрупок для выполнения рефакторинга. Существует более мощная альтернатива, которая позволяет явным образом вызывать конструктор. Она также может использоваться для работы с классами, которые не имеют традиционных конструкторов. Регистрация компонентов с помощью блоков кода Экземпляры некоторых классов не могут быть созданы посредством открытого конструктора. Вместо него для создания экземпляров типов вы должны использовать некоторого рода фабрику. Это всегда проблематично для DI-контейнеров, поскольку по умолчанию они следят за наличием открытых конструкторов. Рассмотрите приведенный ниже пример конструктора класса JunkFood : internal JunkFood(string name) 404 Даже если класс JunkFood является открытым, конструктор расположен внутри него. Очевидно, экземпляры JunkFood должны создаваться посредством статического класса JunkFoodFactory : public static class JunkFoodFactory { public static IMeal Create(string name) { return new JunkFood(name); } } С точки зрения Castle Windsor это проблемное API, поскольку в нем отсутствуют точно выраженные и заданные соглашения касательно статических фабрик. Т ут требуется помощь – и мы можем предоставить ее посредством блока кода, что продемонстрировано в следующем листинге. Листинг 10-11: Конфигурирование метода фабрики container.Register(Component .For .UsingFactoryMethod(() => JunkFoodFactory.Create("chicken meal"))); Для определения блока кода, который создает соответствующий экземпляр, можно использовать метод UsingFactoryMethod – в данном примере путем вызова метода Create с необходимым параметром для JunkFoodFactory Данный блок кода будет вызываться в соответствующее время согласно сконфигурированно му стилю существования компонента. В данном примере, поскольку вы явным образом не определили стиль существования, по умолчанию используется Singleton, а метод Create будет вызыват ься всего единожды, вне зависимости от того, сколько раз вы разрешаете IMeal . Если бы вы сконфигурировали компонент таким образом, чтобы для него использовался стиль существования Transient, то метод Create вызывался бы всякий раз, когда вы разрешали бы IMeal Использование блока кода предоставляет возможность более экзотичной инициализации объектов, нежели обычные открытые конструкторы. Кроме того, используя блок кода, мы получаем обеспечивающу ю наибольшую типовую безопасность альтернативу применению простых типов, нежели та, которую предоставляет метод DependsOn , который вы наблюдали в разделе 10.4.1 "Конфигурирование простых зависимостей": container.Register(Component .For .UsingFactoryMethod(() => new ChiliConCarne(Spiciness.Hot))); В данном примере вы используете блок кода, чтобы явным образом создать новый экземпляр класса ChiliConCarne с необходимым Spiciness . Это обеспечивает большую типовую безопасность, но полностью устраняет возможность автоматической интеграции для рассматриваемого типа. 405 П одсказка Существуют более продвинутые перегрузки UsingFactoryMethod , которые позволяют разрешать зависимости из контейнера. Это полезно в ситуации, когда нам нужно использовать UsingFactoryMethod для того, чтобы явным образом присвоить только один из нескольких параметров, но при этом для выполнения компиляции мы должны передавать все остальные параметры. UsingFactoryMethod – хороший инструмент для работы с классами, которые не могут быть созданы посредством открытого конструктора. Пока у вас есть некое открытое API, которое вы можете вызвать для создания необходимого экземпляра класса, вы можете использовать метод UsingFactoryMethod для того, чтобы явным образом определить блок кода, который будет создавать запрашиваемый экземпляр. Последним общепринятым отклонением от Constructor Injection, которое мы будем здесь наблюдать, является Property Injection. Подключение с помощью Property Injection Property Injection является не столь четко определенной формой механизма внедрения зависимостей, поскольку компилятор не принуждает вас присваивать значение свойству, доступному для записи. И все-таки Castle W indsor, по своей природе, понимает Property Injection и по возможности присваивает значения доступным для записи свойствам. Рассмотрите приведенный ниже класс CaesarSalad : public class CaesarSalad : ICourse { public IIngredient Extra { get; set; } } Согласно общепринятому заблуждению в салат "Цезарь" входит курица. По существу салат "Цезарь" является салатом, но, поскольку с курицей он вкуснее, многие рестораны предлагают возможность добавления в него курицы в качестве дополнительного ингредиента. Класс CaesarSalad моделирует такую возможность посредством доступного для записи свойства под названием Extra Если вы зарегистрируете только класс CaesarSalad без какого-либо Chicken , то свойству Extra не будет присвоено значение: container.Register(Component .For .ImplementedBy Благодаря такой регистрации в результате разрешения ICourse будет возвращаться экземпляр CaesarSalad без какого-либо ингредиента Extra . Т ем не менее, вы можете изменить выходной результат, добавив в контейнер Chicken : container.Register(Component .For .ImplementedBy .For .ImplementedBy 406 Т еперь при разрешении ICourse свойство Extra возвращаемого экземпляра CaesarSalad будет представлять собой экземпляр класса Chicken . Т о есть, Castle Windsor просматривает новый экземпляр на наличие доступных для записи свойств и присваивает им значения, если может предоставить компонент, который по типу совпадает с типом свойства. П одсказка В ситуациях, когда вам необходимо явным образом управлять тем, как присваиваются значения свойствам, вы можете использовать метод ServiceOverrides В данном разделе вы увидели, как работать с API, которые отклоняются от Constructor Injection. Вы можете обратиться к простым аргументам конструктора с помощью метода DependsOn или UsingFactoryMethod , который также поддерживает методы фабрики и другие альтернативы открытых конструкторов. Castle Windsor, по своей природе, поддерживает Property Injection. 407 10.5. Резюме Обзор Castle Windsor, предоставленный в данной главе, только обрисовывает то, что возможно выполнить с помощью одного из самых развитых и исчерпывающих, доступных DI-контейнеров. Поскольку Seam 'ы находятся повсюду, мы можем настроить контейнер для наших собственных нужд, и при этом станут доступными многие дополнительные возможности. В данной главе мы сконцентрировалась на наиболее универсальной части API Windsor. Представленный в ней материал охватывает основное применение контейнера, а также советы по более продвинутым составляющим его API. Это все, что вам нужно знать, если ваша база кода руководствуется DI паттернами и соглашениями. Вооружившись данным знанием, вы должны уметь подключать с помощью Castle Windsor целые приложения. Даже будучи пустым DI-контейнером, Castle W indsor является довольно значительным. Он поддерживает практически любую возможность, которую мы могли бы запросить. Являясь одним из самых старейших .NET DI-контейнеров, он значительно выигрывает благодаря длительным годам своего развития. Однако это не демонстрирует его возраст; напротив, он поддерживает множество новых идей и современных конструкций языка. Все же, возможно, самый значительный недостаток Castle Windsor заключается в том, что огромный набор возможностей реализуется за счет утраты некоторого неоднородного API. Несмотря на то, что начать работать с классом W indsorCont ainer довольно легко, более продвинутые сценарии реализовывать будет сложно до тех пор, пока вы точно не овладеете всем API. К счастью, поскольку форум поддержки Castle Windsor активен, и опытные разработчики выполняют его мониторинг, если у вас появляется вопрос, вы, скорее всего, быстро получите ответ на него. Несмотря на то, что продвинутое API может казаться устрашающим, начать работать с Castle W indsor намного легче, чем с любым другим DI-контейнером: создайте экземпляр W indsorCont ainer, сконфигурируйте его и разрешайте с помощью него компоненты. Существует несколько способов конфигурирования контейнера: возможно и использование кода в качестве конфигурации (code as configuration), и XML, и конфигурация на основании соглашений; мы даже можем сочетать и сопоставлять все эти три способа, чтобы достичь оптимального решения. В Castle Windsor доступен широкий набор стилей существования, включая Singleton, T ransient и Web Request Context. Если не подходят встроенные стили существования, вы можете реализовать пользовательские стили существования – но такое довольно редко случается. Кажется, что поддержка составных компонентов одной и той же абстракции является одной из слабых сторон Castle W indsor. Он, кажется, лучше понимает массивы, чем другие типы последовательностей или списков, но мы можем справиться с этим недостатком довольно легко. Точный способ решения данной проблемы зависит от того, хотим мы разрешить все компоненты или только подмножество компонентов одного и того же сервиса. Несмотря на то, что нам следует полагаться на автоматическую интеграцию, метод ServiceOverrides дает нам возможность явным образом сконфигурировать то, как зависимости присваивают ся компонентам. 408 Иногда компоненты не используют Constructor Injection, а вместо него могут использовать Property Injection или требовать использования отдельных классов фабрики. Т акие сценарии также поддерживаются посредством различных методов. Поскольку Castle W indsor является одним из самых универсальных среди доступных DI- контейнеров, нет причины не использовать его, но он не исключает альтернатив, которые столь же хороши. В следующей главе мы рассмотрим другой развитый и продвинутый DI- контейнер: StructureMap. 409 11. Struc tureMap Меню: Знакомство со StructureMap Управление жизненным циклом Работа с составными компонентами Конфигурирование сложных API В предыдущей главе мы рассматривали DI-контейнер Castle W indsor с целью увидеть, как к нему применяются принципы и паттерны, описанные в частях 1-3. В данной главе мы практически то же самое сделаем и для другого DI-контейнера – StructureMap. StructureMap – старейший DI-контейнер из всех DI-контейнеров для .NET . Он существует дольше, чем все остальные контейнеры. Несмотря на его возраст, он все еще продолжает активно развиваться и обладает множеством современных возможностей, поэтому мы должны рассматривать его возраст, главным образом, как доказательство его развитости. Кроме того, StructureMap является одним из наиболее универсально используемых DI- контейнеров. В данной главе мы рассмотрим то, как можно использовать StructureMap для практического применения принципов и паттернов, изложенных ранее в частях 1-3. Рисунок 11-1 демонстрирует структуру данной главы. Рисунок 11-1: Данная глава разделена на 4 раздела. Первый раздел знакомит нас с API StructureMap и должен рассматриваться в качестве предпосылки следующих трех разделов. Каждый из четырех разделов данной главы может быть прочитан независимо от остальных разделов, несмотря на то, что в четвертом разделе используются методы, которые вводятся в третьем разделе. Данные методы имеют относительно понятные имена, поэтому вы можете прочитать четвертый раздел, не читая при этом третий. Но, с другой стороны, вы можете обнаружить, что вам время от времени придется обращаться к этому разделу. Первый раздел обеспечивает полноценное знакомство со StructureMap и демонстрирует, как конфигурировать и разрешать компоненты. Каждая из трех последующих глав имеет дело с паттернами применения, которые требуют к себе дополнительного внимания. Вы можете читать их по порядку или пропустить некоторые и прочитать только те, которые вам интересны. Данная глава должна дать вам возможность приступить к работе, а также справиться с большинством общепринятых проблем, которые могут возникнуть при ежедневном 410 использовании StructureMap. Это не окончательная трактовка StructureMap, поскольку всецелое его рассмотрение заняло бы несколько глав или, может быть, даже целую книгу. Вы можете прочитать эту главу, не читая при этом остальные главы части 4, специально для того, чтобы познакомиться со StructureMap, или можете прочитать ее совместно с остальными главами части 4, чтобы сравнить DI-контейнеры. Цель данной главы – показать, как StructureMap связан с паттернами и принципами, описанными в частях 1-3, и как он их реализует. 11.1. Знакомство с StructureMap 11.2. Управление жизненным циклом 11.3. Работа с составными компонентами 11.4. Конфигурирование сложных API 11.5. Резюме 411 11.1. Знакомство с StructureMap Из этого раздела вы узнаете, где можно взять StructureMap, что вы при этом получите и как начать его использовать. Кроме того, мы рассмотрим универсальные варианты конфигурирования, а также то, как упаковать настройки конфигурации в повторно используемые компоненты. Т аблица 11-1 предоставляет основополагающу ю информацию, которая, скорее всего, понадобится вам для того, чтобы приступить к работе со StructureMap. Таблица 11-1: Краткая информация о StructureMap Вопрос Ответ |