Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
Откуда мне его получить? Лучше всего начать с сайта ht tp://unity.codeplex.com/ . На главной странице приведены ссылки на самые последние релизы, которые обычно перенаправляют пользователей на веб-узел Microsoft Download Cent er. Из Visual Studio 2010 можно получить его посредством NuGet. Имя пакета – Unity. Что находится в загруженном файле? В отличие от других контейнеров Unity загружается в виде .msi файла. После его установки создаются ярлыки в меню Пуск , а бинарные файлы и исходный код помещаются в папку Program Files . Но поскольку у вас теперь есть бинарные файлы, вы можете развернуть их с помощью Xcopy, если предпочитаете эту форму. Какие платформы поддерживаются? .NET 3.5 SP1 и .NET 4.0. Silverlight 3 и 4. Сколько он стоит? Нисколько. По сути Unity – это программное обеспечение с открытым исходным кодом, несмотря на то, что группа Patterns&Practices не принимает внесенные в исходный код изменения. Откуда мне получить помощь? Unity – это предложение группы Patterns& Practices, а не продукт компании Microsoft. В связи с этим он не сопровождается компанией Microsoft, но зато существует довольно оживленный дискуссионный форум – http://unity.codeplex.com /discussion s На какой версии Unity основана эта глава? 2.0. Как и при работе с Castle Windsor и StructureMap, при использовании Unity соблюдается простая цикличность, проиллюстриро ванная на рисунке 14-2. 534 Рисунок 14-2: Сначала конфигурируется контейнер, а затем из него разрешаются компоненты. В большинстве случаев создается экземпляр класса UnityContainer , который полностью конфигурируется перед началом разрешения компонентов. Компоненты разрешаются из того же экземпляра, который перед этим конфигурировался. После прочтения этого раздела вы приобретете полноценное понимание всей сущности паттерна применения Unity и сможете начать использовать его в сценариях, в которых все компоненты руководствуют ся должными DI-паттернами, например, Constructor Injection. Давайте начнем с простейшего сценария и посмотрим, как можно разрешать объекты с помощью контейнера Unity. Разрешение объектов Основная услуга, предоставляемая любым DI-контейнером – разрешение компонентов. В этом разделе мы рассмотрим API, которое позволяет разрешать компоненты с помощью Unity. Из предыду щих глав вы узнали, что при работе с некоторыми контейнерами (например, Castle Windsor) перед тем, как начать разрешать компоненты, необходимо обязательно явным образом сконфигурировать все компоненты с помощью этого контейнера. В то же время остальные контейнеры (например, StructureMap) полностью понимают, как автоматически интегрировать запрашиваемые компоненты, поскольку они являются конкретными типами с открытыми конструкторами. В сущности, можно сказать, что Unity распознал последнюю из указанных выше категорий, будучи первым DI-контейнером, поддерживающим эту возможность, поэтому самым простым возможным способом использования контейнера Unity является следующий: var container = new UnityContainer(); SauceBéarnaise sauce = container.Resolve Благодаря экземпляру UnityContainer generic-метод Resolve можно использовать для получения экземпляра конкретного класса SauceBéarnaise . Поскольку класс SauceBéarnaise имеет конструктор по умолчанию, Unity автоматически понимает, как создавать экземпляр этого класса. Никакого явного конфигурирования контейнера не требуется. Unity поддерживает механизм автоматической интеграции. Поэтому даже при отсутствии конструктора по умолчанию он сможет создавать экземпляры без предварительно й конфигурации, пока все входящие в конструктор параметры являются конкретными 535 классами и все листья дерева параметров имеют тип, обладающий конструкторами по умолчанию. Рассмотрим в качестве примера конструктор Mayonnaise : public Mayonnaise(EggYolk eggYolk, OliveOil oil) Несмотря на то, что рецепт приготовления майонеза слегка упрощен, и EggYolk , и OliveOil являются конкретными классами, обладающими конструкторами по умолчанию. Сам Mayonnaise не обладает конструктором по умолчанию, но Unity все равно может создавать его без предварительно й конфигурации: var container = new UnityContainer(); var mayo = container.Resolve Это возможно, поскольку Unit y способен разгадать, как создавать все необходимые параметры конструктора. Но как только мы ввели слабое связывание, мы должны сконфигурировать Unit y, преобразовав абстракции в конкретные типы. П реобразование абстракций в конкретны е типы Несмотря на то, что способность Unit y автоматически интегрировать конкретные классы может, наверняка, время от времени быть полезной, для слабого связывания обычно бывает необходимо преобразовывать абстракции к конкретным типам. Создание экземпляров на основании таких преобразований – ключевая услуга, предлагаемая любым DI-контейнером, но вам все равно необходимо задать соответствующее преобразование. В приведенном ниже примере вы преобразуете интерфейс IIngredient в конкретный класс SauceBéarnaise , который позволяет успешно разрешать IIngredient : var container = new UnityContainer(); container.RegisterType IIngredient ingredient = container.Resolve Generic-метод RegisterType – один из тех нескольких методов расширения, которые вызывают слабо типизированный метод RegisterType , заданный для IUnityContainer . В предыдущем примере вы использовали перегрузку, в которой определяли абстракцию, а также конкретный тип как два типовых generic-аргумента. В этом примере вы преобразуете IIngredient в SauceBéarnaise таким образом, чтобы при дальнейшем разрешении IIngredient вы получали экземпляр SauceBéarnaise Generic-метод расширения RegisterType помогает предотвратить появление ошибок конфигурации, поскольку конечный тип обладает generic-ограничителем, который задает условие, согласно которому этот тип наследуется от исходного типа. Код, приведенный в предыдущем примере, компилируется, поскольку SauceBéarnaise реализует IIngredient В большинстве случаев все, что вам нужно – это строго типизированное API, и, поскольку в таком API выполняется необходимая проверка во время компиляции, вам лучше использовать его при любой возможности. Однако встречаются ситуации, когда разрешать сервисы необходимо с помощью более слабо типизированных способов. Т акое тоже возможно. 536 Разрешение слабо типизированных сервисов Иногда у вас нет возможности использовать generic API, поскольку на этапе проектирования вы еще не знаете, какой тип вам понадобится. У вас есть только экземпляр Type , но все же вам хотелось бы получить экземпляр этого типа. Пример такой ситуации описан в разделе 7.2 "Построение ASP.NET MVC приложений", в котором обсуждался ASP.NET класс DefaultControllerFactory . Соответствующий метод приведен в следующем примере: protected internal virtual IController GetControllerInstance( RequestContext requestContext, Type controllerType); Поскольку у вас имеется только экземпляр Type , вы не можете использовать generic'и, а должны прибегнуть к слабо типизированному API. К счастью, Unity имеет слабо типизированную перегрузку метода Resolve , которая позволяет реализовывать метод GetControllerInstance следующим образом: return (IController)this.container.Resolve(controllerType); Слабо типизированная перегрузка метода Resolve позволяет передавать аргумент controllerType напрямую в Unit y, но при этом вам необходимо явным образом приводить возвращаемое значение к IController Независимо от того, какую из перегрузок метода Resolve вы используете, Unity гарантирует, что этот метод будет возвращать экземпляр запрашиваемого типа или выдавать исключение при наличии зависимостей, неудовлетворяющих условиям. После того, как все необходимые зависимости должным образом сконфигурированы, Unity может выполнить автоматическую интеграцию запрашиваемого типа. В предыдущем примере this.container – это экземпляр IUnityContainer . Чтобы можно было разрешать запрашиваемый тип, сначала необходимо сконфигурировать все слабо связанные зависимости. Существует множество способов конфигурирования контейнера Unity, а в следующем разделе приведен обзор наиболее универсальных из этих способов. Конфигурирование контейнера Как уже говорилось в разделе 3.2 "Конфигурирование DI-контейнеров", существует несколько концептуально разных способов конфигурирования DI-контейнера. На рисунке 14-3 представлен обзор возможных вариантов. 537 Рисунок 14-3: Концептуально разные варианты конфигурирования. Т ехнология конфигурирования в коде подразумевает строгую типизированность и имеет тенденцию к явному определению. XML, с другой стороны, предполагает позднее связывание, но тоже склонен к явному определению. Автоматическая регистрация, напротив, полагается на соглашения, которые могут быть и строго типизированными, и более слабо определенными, но в Unity не встроена поддержка механизма автоматической регистрации. Несмотря на то, что Unity полностью поддерживает XML-конфигурацию, в его основе лежит императивная конфигурация. XML-конфигурация реализуется в произвольной библиотеке, для использования которой необходимо явным образом на нее ссылаться. Хотя Unity поддерживает и конфигурацию в коде, и XML-конфигурацию, в него не встроена поддержка основанного на соглашениях механизма автоматической регистрации. Однако, как мы уже ранее упоминали, мы всегда можем реализовать соглашение посредством написания пользовательс кого императивного кода. В этом разделе мы будем рассматривать способы применения различных вариантов конфигурации. Конфигурация в коде В разделе 14.1.1 "Разрешение объектов" уже приводился краткий обзор строго типизированного API конфигурации контейнера Unity. В этом разделе мы рассмотрим его более подробно. За исключением некоторых моментов вся конфигурация контейнера Unity выполняется в слабо типизированном методе RegisterType , определенном для IUnityContainer : 538 IUnityContainer RegisterType(Type from, Type to, string name, LifetimeManager lifetimeManager, params InjectionMember[] injectionMembers); Помимо этого метода в состав Unity входят еще несколько методов расширений. Некоторые из них являются строго типизированными generic-методами, а некоторые – слабо типизированными методами. В этой главе мы сосредоточим все свое внимание на строго типизированном API. Одним из наиболее часто используемых методов является приведенная ниже перегрузка: container.RegisterType В отличие от Castle Windsor или Autofac преобразование IIngredient в SauceBearnaise , приведенное в предыдущем примере, не избавляет нас от разрешения самого SauceBearnaise . И sauce , и ingredient будут соответствующим образом разрешаться в следующем коде: container.RegisterType Возможно, из разделов 10.1.2 "Конфигурирование контейнера" и 13.1.2 "Конфигурирование ContainerBuilder" вы помните, что преобразование IIngredient в SauceBearnaise с помощью Castle W indsor или Autofac будет приводить к исчезновению конкретного класса ( SauceBearnaise ) до тех пор, пока вы не предпримите дополнительные шаги. При работе с Unit y никаких дополнительных шагов предпринимать не нужно, поскольку Unity позволяет разрешать и IIngredient , и SauceBearnaise . В большинстве случаев возвращаемые объекты – это экземпляр ы SauceBearnaise В реальных приложениях нам всегда приходится преобразовывать более одной абстракции, поэтому нам необходимо сконфигурировать несколько преобразований. Сделать это можно с помощью нескольких вызовов метода RegisterType : container.RegisterType Здесь выполняется преобразование IIngredient в SauceBearnaise и преобразование ICourse в Course . Никаких наложений типов не происходит, поэтому все, что происходит дальше должно быть довольно очевидным. Кроме того, вы можете попытаться зарегистрировать одну и ту же абстракцию несколько раз, но, если вы сделаете это так, как приведено в примере ниже, то произойдет что-то неожиданное: container.RegisterType Регистрация типа без имени определяет значение по умолчанию для этого типа, но при этом переопределяется предыдущ ее значение по умолчанию. Конечным результатом этого примера является то, что, если вы будете разрешать IIngredient , то получите экземпляр SauceBearnaise , но регистрация Steak при этом будет уже невозможна. Для типа возможно только одно значение по умолчанию, но зарегистрировать вы можете сколько угодно именованных компонентов. Чтобы использовать Steak в качестве IIngredient по умолчанию, вы можете зарегистрировать SauceBearnaise , присвоив ему имя: 539 container.RegisterType Steak остается IIngredient по умолчанию, но, кроме того, вы можете разрешить IIngredient в SauceBearnaise , запросив IIngredient с именем sauce . Чтобы точно указать, как будут интегрироваться зависимости, можно воспользоваться именованными компонентами. К этой теме мы вернемся в разделе 14.3.1. Возможны и другие способы конфигурирования Unity, но с помощью приведенных в этом разделе методов можно полноценно сконфигурировать приложение. Чтобы оградить себя от слишком явного сопровождения конфигурации контейнера, было бы просто замечательно вместо этих методов использовать другой подход, основанный на соглашениях – автоматическу ю регистрацию. Автоматическая регистрация Чаще всего большинство регистраций аналогичны друг другу. Т акие регистрации очень утомительно сопровождать, а явная регистрация каждого компонента, возможно, не является самым продуктивным подходом. Рассмотрим библиотеку, которая содержит реализации интерфейса IIngredient . Можно выполнять индивидуальну ю регистрацию каждого класса, но это приведет к многочисленным схожим вызовам метода RegisterType . Что еще хуже, всякий раз при добавлении новой реализации IIngredient необходимо явно регистрировать ее вместе с контейнером, чтобы она была доступна для использования. Было бы более продуктивно установить, что все реализации IIngredient , найденные в этой сборке, необходимо зарегистрировать. К несчастью, в Unit y не встроена поддержка автоматической регистрации, но, поскольку Unity имеет комплексное императивное API, мы можем написать пользовательский код, чтобы достичь того же эффекта. В следующем листинге мы рассмотрим простой пример, который демонстрирует, как этого можно достичь. Это никоим образом не является окончательной трактовкой этой темы, а скорее зарисовкой возможных вариантов. Чтобы просмотреть сборку и зарегистрировать все реализации интерфейса IIngredient , можно воспользоваться комбинацией рефлекционного API .NET и слабо типизированного метода RegisterType , как это продемонстрировано в следующем листинге. Листинг 14-1: Регистрация всех IIngredient сборки foreach (var t in typeof(Steak).Assembly.GetExportedTypes()) { if (typeof(IIngredient).IsAssignableFrom(t)) { container.RegisterType(typeof(IIngredient), t, t.FullName); } } Из этой сборки вы можете вытащить перечень всех открытых типов и выбрать только те из них, которые напрямую или косвенно реализуют интерфейс IIngredient . После применения фильтра для регистрации каждого типа IIngredient относительно интерфейса можно воспользоваться слабо типизированным методом Register . Чтобы предотвратить перетирание регистраций, необходимо не забывать присваивать каждой из 540 них уникальное имя. В этом примере вы используете полное имя каждого конкретного класса, но подойдет и что-нибудь другое, гарантирующее уникальность имени. Несмотря на то, что Unity не обладает API, которое способно было бы справляться с основанным на соглашениях механизмом автоматической регистрации, для достижения того же эффекта, можно было бы написать свой собственный код. Конечно, предпочтительнее было бы иметь встроенную поддержку этого механизма, но, по крайней мере, API контейнера Unity не мешает нам определять соглашения вручную. П одсказка Проект с открытым исходным кодом Unity Auto Registration – одна из попыток определить повторно используемое API, которое позволило бы использовать механизм автоматической регистрации в Unity. Будь мы более великодушны, мы могли бы сказать, что отсутствие API автоматической регистрации означает для нас отсутствие всяких ограничений. Если мы можем что-то закодировать, значит, мы можем это получить. Если бы мы захотели просмотреть папку на факт наличия в ней сборок или выполнить поиск типов в каждой сборке, то мы даже смогли бы реализовать дополнительную функциональность, при которой можно было бы добавлять в приложение расширения без повторной компиляции основного приложения. Это был бы один из способов реализации позднего связывания. Еще один способ – применение XML-конфигурации. XML-конфигурация XML-конфигурация является подходящим решением в тех ситуациях, когда нам необходимо уметь изменять конфигурацию без повторной компиляции приложения. П одсказка Рекомендую использовать XML-конфигурацию только для тех типов, изменять которые можно без повторной компиляции приложения. В остальных случаях используйте технологию конфигурирования в коде. Unity подразумевает, что мы размещаем XML-конфигурацию в конфигурационном файле приложения. Для загрузки и интерпретации XML-конфигурации в Unity используется стандартное .NET API конфигурации. П одсказка Поддержка контейнером Unity возможности выполнения XML-конфигурации полностью сравнима с аналогичной возможностью других DI-контейнеров. В нем даже присутствует XSD-файл, который применяется для того, чтобы разрешить использовать в Visual Studio контекстную подсказку IntelliSense. П римечание Поскольку возможность поддержки контейнером Unity XML-конфигурации реализована в виде отдельной сборки, для использования этой возможности необходимо добавить ссылку на сборку Microsoft.Practices.Unity.Configuration 541 После добавления ссылки на сборку Microsoft.Practices.Unity.Configuration мы должны добавить директиву using для пространства имен Microsoft.Practices.Unity.Configuration , чтобы нам стал доступен метод расширения LoadConfiguration . Это позволяет нам загружать XML-конфигурацию посредством всего лишь одного вызова метода: container.LoadConfiguration(); Метод LoadConfiguration загружает XML-конфигурацию из стандартного конфигурационного файла приложения в контейнер. П римечание К сожалению, мы не можем считывать XML из других источников, например, из потоков или XML-узлов, поскольку соответствующее API в Unity отсутствует. П одсказка Несмотря на то, что мы не можем считывать XML из произвольных источников, мы можем считывать XML из любого конфигурационного файла с помощью API ConfigurationManager Чтобы разрешить выполнять конфигурацию контейнера Unity в конфигурационном файле, мы должны сначала добавить раздел конфигурации, используя стандартное .NET API конфигурации, предназначенно е для определения пользовательских разделов конфигурации: Это API позволяет нам добавить в конфигурационный файл раздел конфигурации unity Ниже приведен простой пример преобразования интерфейса IIngredient в класс Steak : XML-схема контейнера Unity позволяет определять осмысленные значения по умолчанию, которые, возможно, помогут сократить большие объемы кода, появляющиеся при работе с квалифициро ванными именами типов сборки в XML. Несмотря на то, что добавлять элементы namespace не обязательно, мы можем добавлять их в неограниченном количестве. Элементы namespace эквивалентны директивам using языка C#. В приведенном выше примере кода мы добавляем только одно пространство имен Ploeh.Samples.MenuModel , но могли бы добавить и большее количество таких элементов или же вовсе их опустить. Если мы опускаем элемент namespace , то все равно можем 542 явным образом передавать полностью квалифициро ванно е имя типа в виде составляющей части регистрации. Работа с элементом assembly аналогична работе с элементом namespace . М ы можем добавлять неограниченное количество элементов assembly или же совсем их опускать. В приведенном выше примере кода мы добавляем сборку Ploeh.Samples.MenuModel , в которой определяются интерфейс IIngredient и класс Steak Это позволяет нам четко устанавливать соответствие между IIngredient и Steak с помощью элемента register . Поскольку мы добавили в контекст пространства имен и сборки, мы можем ссылаться на IIngredient и Steak по их сокращенным именам. Пока имена в рамках контекста четко определены, Unity раскрывает их за нас подобно тому, как это делает компилятор C#. XML-конфигурация является подходящим решением в тех ситуациях, когда нам необходимо изменить конфигурацию одного или нескольких компонентов без повторной компиляции приложения. Но поскольку такой подход является довольно хрупким, мы должны использовать его только для таких случаев, а для остальной части конфигурации контейнера использовать технологию конфигурирования в коде. П одсказка Вы не забыли, что выигрывает последняя конфигурация? Этот факт вы можете использовать для того, чтобы переписать жестко закодированную конфигурацию с помощью XML-конфигурации. Для этого вы должны не забыть загрузить XML- конфигурацию после конфигурации всех остальных компонентов. И наоборот, если у вас имеется конфигурация, которую нельзя переписывать с помощью XML, примените эту конфигурацию после загрузки XML-конфигурации. В этом разделе мы, главным образом, рассматривали различные API регистрации контейнера Unit y. Несмотря на то, что вы, наверняка, можете записать один большой блок неструктурированного кода конфигурации, лучше всего разбить конфигурацию на модули. Хотя контейнер Unity явным образом не поддерживает такую возможность, оказывается, мы все равно можем достичь такого же результата. Пакетирование конфигурации Иногда существует необходимость упаковать логику конфигурации в повторно используемые группы. И даже в тех случаях, когда само по себе повторное использование не является высшим приоритетом, при конфигурировании больших и сложных приложений может появиться необходимость создать некоторого рода структуру. В контейнере Castle W indsor есть инсталлеры, в StructureMap – регистры, а в Autofac – модули. Но в Unity нет ничего похожего. В нем отсутствует интерфейс, который был бы предназначен, главным образом, для пакетирования конфигурации в повторно используемые компоненты. Но зато в нем есть более или менее достаточная замена этим механизмам: |