Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
видовой субдомен. О пределение DI-контейнер – это библиотека, которая обеспечивает функциональност ь механизма внедрения зависимостей. П римечание DI-контейнеры также называют IoC-контейнерами или (намного реже) Легковесными конт ейнерам и. Несмотря на то, что вам нужно обращаться к инфраструктуре приложения, выполнение этого само по себе не добавляет никакой бизнес-значимости, поэтому лучше всего использовать общецелевую библиотеку. Она не отличается от реализации входа в систему или доступа к данным. Регистрация данных приложения – это проблема, к которой чаще всего обращается общецелевая библиотека управления логами. Т о же самое верно и для диаграмм компоновки объектов. П редупреждение Не ждите, что DI-контейнер волшебным образом превратит сильно связанный код в слабо связанный. DI-контейнер может сделать использование механизма внедрения зависимостей более эффективным, но приложение, в первую очередь, должно быть спроектировано с учетом DI паттернов и технологий. В данном разделе я буду обсуждать то, как DI-контейнеры формируют диаграммы объектов и продемонстрирую вам некоторые примеры, чтобы дать вам общее представление того, на что должно быть похоже использование контейнера. Контейнер "Hello" DI-контейнер – это библиотека программных средств, похожая на любую другую библиотеку программных средств. Он предоставляет API, которое вы можете использовать для компоновки объектов. Формирование диаграммы объектов – это вызов единичного метода. Все DI-контейнеры необходимо конфигурировать перед использованием их для компоновки объектов, но я заново пересмотрю этот вопрос в разделе "Конфигурирование DI-контейнеров". В этом разделе я продемонстрирую вам несколько примеров того, как DI-контейнеры могут преобразовывать диаграммы объектов для расширенного шаблонного приложения из раздела "Р асширение шаблонного приложения". Для каждого запроса ASP.NET MVC фреймворк будет запрашиват ь экземпляр соответствующего типа IController , поэтому 84 вы должны реализовать метод, который использует DI-контейнер для формирования соответствующей диаграммы объектов. П одсказка Раздел "Построение ASP.NET MVC приложений" содержит подробную информацию о том, как формировать ASP.NET MVC приложения. MVC фреймворк будет вызывать метод для экземпляра Type , который определяет нужный ему тип IController (например, HomeController или BasketController ), а вы должны возвращать экземпляр этого типа. Эта функциональнос ть может быть реализована для всех DI-контейнеров, рассматриваемых в части 4, но в данном разделе я продемонстрирую только несколько примеров. П реобразование контроллеров для различны х DI-конте йнеров Unity – это DI-контейнер с явно соответствующим паттерну API. Предполагая, что у вас уже есть экземпляр класса UnityContainer контейнера Unity, вы можете преобразовать экземпляр IController из аргумента controllerType типа Type : var controller = (IController)this.container.Resolve(controllerType); Вы будете передавать параметр controllerType в метод Resolve и получать экземпляр требуемого типа, полностью заполненный всеми подходящими зависимостями. Поскольку слабо-типизированный метод Resolve возвращает экземпляр System.Object , он должен быть отправлен в IController Для таких случаев, когда вы уже на этапе проектирования знаете, какой тип необходим, существует также общая версия метода Resolve Многие DI-контейнеры обладают API, которое похоже на API контейнера Unity. Соответствующий код для Castle Windsor выглядит идентично коду Unit y, несмотря на то, что экземпляр container будет уже экземпляром класса WindsorContainer . Остальные контейнеры имеют несколько другие названия – для StructureMap, например, предыдущий код будет выглядеть следующим образом: var controller = (IController)this.container.GetInstance(controllerType); Единственное реальное отличие – метод Resolve называется GetInstance . Вы можете извлечь из этих примеров общий вид DI-контейнера. Диаграммы преобразования объектов для DI-конте йнеров DI-контейнер – это движок, который преобразует и управляет диаграммами объектов. Несмотря на то, что DI-контейнер выполняет множество других функций, помимо преобразования объектов, преобразование объектов является центральной частью API любого контейнера. Предыду щие примеры демонстрируют, что у контейнеров для этих целей есть слабо-типизированный метод. Варьируясь в названиях и сигнатурах, в общем случае он выглядит следующим образом: 85 object Resolve(Type service); Как демонстрируют предыдущие примеры, поскольку возвращаемый экземпляр является типизированным, как, например, System.Object , то вам часто приходится преобразовывать возвращаемо е значение к необходимому типу перед тем, как его использовать. Многие DI-контейнеры также для этих случаев предлагают общую версию, когда мы знаем, какой тип запрашивается во время компиляции. Они часто выглядят следующим образом: T Resolve Вместо применения аргумента Type метода такая перегрузка принимает типизированный параметр ( T ), который указывает на необходимый тип. Метод возвращает экземпляр T Большинство контейнеров выдают исключения, если не могут преобразовать необходимый тип. П редупреждение Сигнатура метода Resolve чрезвычайно влиятельная и универсальная. Вы можете запросить экземпляр любого типа, и ваш код все еще будет компилируемым. В действительности, метод Resolve устанавливает сигнатуру анти-паттерна Service Locator, и вам нужно действовать осторожно, чтобы не использовать свой DI-контейнер в качестве Service Locator. Если мы рассмотрим метод Resolve изолированно, то он выглядит почти волшебным. С точки зрения компилятора, можно попросить его преобразовать экземпляры произвольных типов. Каким образом контейнер узнает, как формировать необходимые типы, включая все зависимости? Он не знает этого, и вам придется сначала ему это разъяснить. Вы делаете это посредством регистрации или конфигурирования, и именно здесь вы преобразуете абстракции в конкретные типы – я вернусь к этому вопросу в разделе "Конфигурирование DI-контейнеров". Если у контейнера не будет подходящей конфигурации для того, чтобы полноценно сформировать необходимый тип, он будет выдавать описательное исключение. Например, Castle W indsor имеет следующие примерные сообщения- исключения: Невозможно создат ь компонент "Ploeh.Samples.MenuModel.Mayonnaise", поскольку у него есть зависим ости, кот орые необходимо выделит ь. Для Ploeh.Samples.MenuModel.Mayonnaise нужны следующ ие зависимост и: Сервисы: – Ploeh.Sam ples.MenuModel.EggYolk, кот орый не был зарегистрирован. В данном примере вы можете увидеть, что Castle Windsor не может преобразовать Mayonnaise , поскольку он не был настроен для работы с классом EggYolk Если контейнер корректно сконфигурирован, то он может преобразовывать даже сложные диаграммы объектов из требуемых типов. Если в конфигурации что-то упущено, то 86 контейнер может предоставить детальную информацию о том, что пропущено. В следующем разделе мы подробнее рассмотрим, как это делается. Автоматическая интеграция DI-контейнеры бурно развиваются на основе статической информации, собранной во всех классах, использующих технологию внедрения через конструктор. Используя рефлексию, они могут анализировать запрашиваемые классы и предугадывать, какие зависимости им нужны. Некоторые DI-контейнеры понимают и паттерн Propert y Injection (внедрение зависимостей через свойства), но все они, по своему существу, понимают паттерн Constructor Injection и формируют диаграммы объектов путем сочетания их собственной конфигурации с информацией, извлеченной из конструкторов классов. Данный процесс называется автоматической интеграцией. О пределение Автоматическая интеграция – это способность автоматически формировать диаграмму объектов из таблицы преобразований между абстракциями и конкретными типами. Рисунок 3-4 описывает общий алгоритм, которому следует большинство DI-контейнеров, чтобы автоматически интегрировать диаграмму объектов. DI-контейнер будет использовать эту конфигурацию для того, чтобы найти соответствующий конкретный класс, совпадающий с запрашиваемым типом. Затем DI-контейнер использует рефлексию для изучения конструктора класса. Если существует конструктор по умолчанию, то DI- контейнер будет вызывать конструктор и возвращать созданный экземпляр. Рисунок 3-4: Упрощенная последовательность действий для автоматической интеграции. DI-контейнер будет рекурсивно находить конкретные типы и изучать их конструкторы до тех пор, пока он не сможет создать целостное дерево объектов. 87 Если для конструктора нужны аргументы, то начинается рекурсивный процесс, во время которого DI-контейнер будет повторять этот процесс для каждого типа аргумента до тех пор, пока все конструкторы не будут просмотрены. В разделе "Конфигурирование DI-контейнеров" мы подробнее рассмотрим то, как можно конфигурировать контейнеры, а сейчас самое главное – это понять, что центральной частью конфигурации является то, как различные абстракции преобразуются в конкретные классы. Все это звучит слегка теоретически (думаю, что слово "абстракция" не поможет), поэтому я думаю, что пример будет очень полезным. П ример: Автоматическая интеграция BasketC ontroller В этом примере я буду объяснять, как, в принципе, работает автоматическая интеграция. Пример не опирается ни на какой конкретный DI-контейнер, но вместо этого предоставляет обзор того, как контейнеры формируют диаграммы объектов. Представьте себе, что вам нужно преобразовать экземпляр класса BasketController . Вы делаете это путем вызова метода Resolve для typeof(BasketController) . В итоге вам хотелось бы получить экземпляр BasketController , сформированный так, как это показано на рисунке 2-26. Чтобы достичь этого, вы должны сначала убедиться, что контейнер имеет корректную конфигурацию. Т аблица 3-1 демонстрирует, как эта конфигурация преобразует абстракции в конкретные типы. Кроме того, я добавил столбец, который показывает, является ли абстракция интерфейсом или абстрактным базовым классом – с точки зрения DI-контейнера это очень важно, но я думал, что это поможет прояснить то, что происходит. Таблица 3-1: Преобразование типов для обеспечения автоматической интеграции BasketController Т ип абстракции Абстракция Конкретный тип Явный BasketController BasketController Интерфейс IBasketService BasketService Абстрактный класс BasketRepository SqlBasketRepository Абстрактный класс BasketDiscountPolicy RepositoryBasketDiscountPolicy Абстрактный класс DiscountRepository SqlDiscountRepository Строка connString "metadata=res://*/Com merceModel.csdl| […]" Когда DI-контейнер получит запрос BasketController , первое, что он сделает – будет искать тип в его конфигурации. BasketController – это конкретный класс, поэтому он преобразуется в самого себя. Затем контейнер использует рефлексию для осмотра конструктора BasketController . Из раздела "Возможность добавления в корзину" вы можете помнить, что BasketController обладает единственным конструктором со следующей сигнатурой: public BasketController(IBasketService basketService) Поскольку этот конструктор не является конструктором по умолчанию, нам необходимо повторить этот процесс для аргумента конструктора IBasketService , если мы руководствуемся общей блок-схемой, изображенной на рисунке 3-4. 88 Контейнер ищет IBasketService в своей конфигурации и обнаруживает, что IBasketService преобразуется в конкретный класс BasketService . Единственный открытый конструктор BasketService имеет следующу ю сигнатуру: public BasketService(BasketRepository repository, BasketDiscountPolicy discountPolicy) Это все еще не конструктор по умолчанию, и теперь вы работаете с аргументами двух конструкторов. Контейнер осматривает каждый из аргументов по порядку, поэтому он начинает с класса BasketRepository , который согласно конфигурации преобразуется в SqlBasketRepository SqlBasketRepository обладает открытым конструктором со следующей сигнатурой: public SqlBasketRepository(string connString) Единственный аргумент конструктора – это строковый параметр под названием connString , который сконфигурирован таким образом, что обладает конкретным значением. Т еперь, когда у контейнера есть подходящее значение, он может вызвать конструктор SqlBasketRepository . На данный момент контейнер успешно обрабатывал параметр repository конструктора BasketService , но ему понадобится придержать это значение ненадолго, поскольку ему также нужно позаботиться и о параметре discountPolicy Согласно конфигурации BasketDiscountPolicy преобразуется в конкретный класс RepositoryBasketDiscountPolicy , который обладает следующим открытым конструктором: public RepositoryBasketDiscountPolicy(DiscountRepository repository) Выполняя поиск DiscountRepository в своей конфигурации, контейнер обнаруживает, что он DiscountRepository преобразуется в SqlDiscountRepository , который обладает следующим конструктором: public SqlDiscountRepository(string connString) Ситуация совпадает с той, когда вы сталкивались с SqlBasketRepository . Аргумент connString преобразуется в конкретную строку соединения, которую контейнер может передать в конструктор. На данный момент контейнер передает новый экземпляр SqlDiscountRepository в конструктор RepositoryBasketDiscountPolicy . Наряду с SqlBasketRepository , он теперь выполняет конструктор BasketService и вызывает его посредством рефлексии. В конце концов, он передает вновь созданный экземпляр BasketService в конструктор BasketController и возвращает экземпляр BasketController По существу это и есть то, как работает автоматическая интеграция, несмотря на то, что она является более сложной, чем описанный процесс. DI-контейнерам также необходимо заботиться об управлении жизненным циклом и, возможно, обращаться к технологии внедрения через свойства, а также к другим более специфичным необходимым условиям создания зависимостей. Основной момент – это то, что технология внедрения через конструктор статически объявляет требования зависимости о наличии класса, а DI- 89 контейнер использует эту информацию для того, чтобы автоматически интегрировать диаграммы сложных объектов. Как показывает пример, контейнер нужно сконфигурировать перед тем, как он сможет формировать диаграммы объектов. Регистрация компонентов может быть выполнена различными способами. 90 3.2. Конфигурирование DI-контейнеров Несмотря на то, что метод Resolve – это место, где выполняются все действия, вы должны понимать, что потратите больше времени на конфигурацию API DI-контейнера. Преобразование диаграмм объектов, в конечном счете, – это вызов единственного метода. DI-контейнеры поддерживают две или три общие опции конфигурации, продемонстрированные на рисунке 3-5. Некоторые из них не поддерживают автоматическую регистрацию, один из контейнеров не поддерживает технологию использования кода в качестве конфигурации, в то время как XML конфигурация используется повсюду. Большинство контейнеров позволяют вам сочетать несколько подходов в одном приложении. Рисунок 3-5: Наиболее универсальные способы конфигурирования DI-контейнера, показанные относительно таких параметров, как ясность и степень связывания. Эти три опции конфигурации имеют разные характеристики, которые делают их полезными в различных ситуациях. И XML конфигурация, и использование кода в качестве конфигурации являются явно определенными, поскольку они требуют, чтобы мы регистрировали каждый компонент в индивидуальном порядке. Автоматическая регистрация, с другой стороны, является более неявной, поскольку при этом подходе используются условные обозначения для регистрации набора компонентов с помощью единого правила. При использовании кода в качестве конфигурации мы компилируем конфигурацию контейнера в сборку, тогда как XML конфигурация позволяет нам поддерживать позднее связывание, при котором мы можем изменять конфигурацию без необходимости заново компилировать приложение. В данном измерении автоматическая регистрация ниспадает где-то до середины, поскольку мы можем попросить просмотреть единственную сборку, 91 которая известна во время компиляции или, в противном случае, просмотреть все сборки в предопределенной папке. В таблице 3-2 перечисляются преимущества и недостатки каждой опции. Таблица 3-2: Опции конфигурации Стиль Описание Преимущества Недостатки XML Настройки конфигурации (часто в .config файлах) определяют преобразования. Обеспечивает возможность замены без повторной компиляции. Высокая степень контроля. Отсутствуют проверки во время компиляции. Очень подробный. Использование кода в качестве конфигурации Код явно определяет преоразования. Проверки во время компиляции. Высокая степень контроля. Не поддерживает возможность замены без повторной компиляции. Автоматическая регистрация Для определения местоположения подходящих компонентов и построения преобразований используются правила. Обеспечивает возможность замены без повторной компиляции. Необходимы минимальные усилия. Помогает принудительным соглашениям сделать базу кода более логичной. Частичные проверки во время компиляции. Наименьший контроль. Т радиционно DI-контейнеры начались с XML конфигурации, что также объясняет тот факт, что все контейнеры поддерживают данную опцию. Тем не менее, тенденцией настоящего времени является то, что эта возможность приуменьшается в пользу более условных подходов. Несмотря на то, что автоматическая регистрация – это самая современная опция, это не самое очевидное место для старта. Из-за своей неявности эта возможность может казаться более абстрактной, нежели другие более явные опции, поэтому я буду рассматривать каждую опцию в историческом порядке, начиная с XML конфигурации. Конфигурирование контейнеров при помощи XML Когда DI-контейнеры впервые появились в ранние 2000-е годы, все они использовали XML в качестве механизма конфигурации – с тех пор многое изменилось. Более частое использование XML в качестве механизма конфигурации в дальнейшем раскрыло тот факт, что такой подход изредка является самым лучшим. XML является довольно подробным и хрупким. При конфигурировании DI-контейнера в XML вы идентифициру ете различные классы и интерфейсы, но ваш компилятор не поддерживает возможность предупреждения вас о том, что вы что-то пропустили. Даже если имена классов корректны, нет никакой гарантии, что необходимая сборка будет находиться в исследуемом пути приложения. Преимущество XML конфигурации – вы можете изменять поведение приложения без повторной компиляции. Это важно, если вы разрабатываете программное средство, 92 которое поставляется тысячам покупателей, потому что это дает им возможность самостоятельно настроить приложение. Как бы то ни было, если вы пишите внутреннее приложение или веб-сайт, в котором вы управляете средой развертывания, чаще всего бывает легче просто перекомпилировать и развернуть заново приложение в тех случаях, когда вам нужно изменить поведение. П одсказка Используйте XML конфигурацию только в тех случаях, когда вам очень хочется обеспечить позднее связывание. Во всех остальных случаях отдайте предпочтение стилю "код в качестве конфигурации" или автоматической регистрации. DI-контейнер часто конфигурируется с помощью XML посредством направления его в конкретный XML-файл, но иногда он также может подхватить конфигурацию и из конфигурационного файла приложения. В следующих примерах используется последняя указанная опция. П ример: Конфигурирова ние шаблонного коммерческого приложения с помощью XML Поскольку контейнер Unity является одним из самых XML-центрированных DI- контейнеров, рассматриваемых в данной книге, имеет смысл использовать его для примера XML конфигурации. В этом примере вы будете конфигурировать шаблонное коммерческое приложение из раздела "Расширение шаблонного приложения". Самая большая составляющая задачи – применить конфигурацию, которая законспектирована в таблице 3-1, но кроме того вы должны применить похожую конфигурацию для того, чтобы обеспечить композицию класса HomeController . Следующий листинг демонстрирует конфигурацию, необходимую для поднятия и запуска приложения. Листинг 3-1: Конфигурирование Unity при помощи XML 1. 2. 3. 4. 5. 6. typeConverter="ConnectionStringConverter" /> 8. 9. 10. 11. 12. 13. 14. typeConverter="ConnectionStringConverter" /> 16. 17. 18. 19. 20. 21. 22. typeConverter="ConnectionStringConverter" /> 93 24. 25. 26. 27. 28. 29. 30. typeConverter="ConnectionStringConverter" /> 32. 33. 34. Строка 4-9: Определяет строку соединения Строка 1: Простое преобразование Как вы можете видеть даже из этого простого листинга кода, XML конфигурация достаточно подробна. Простые преобразования, подобные преобразованию интерфейса IBasketService в класс BasketService , легко выражаются с помощью простого элемента register Т ем не менее, как вы, возможно, помните, некоторые конкретные классы принимают строку соединения в качестве входных данных, поэтому вам необходимо определить, каким образом находится значение этой строки. Что касается Unity, вы можете сделать это, указав, что вы используете пользовательский тип конвертера под названием ConnectionStringConverter . Этот конвертер будет искать значение CommerceObjectContext среди стандартных строк соединения web.config и возвращать строку соединения с этим именем. Оставшиеся элементы повторяют эти два паттерна. Поскольку Unity может автоматически преобразовывать запросы в конкретные типы, даже если отсутствуют явные регистрации, вам не нужно применять XML элементы для HomeController и BasketController Загрузка конфигурации в контейнер выполняется при помощи вызова единственного метода: container.LoadConfiguration(); Метод LoadConfiguration загружает XML конфигурацию из листинга 3-1 в контейнер. После размещения конфигурации контейнер теперь может преобразовыват ь запросы в HomeController и в другие. Остальные DI-контейнеры также поддерживают XML конфигурацию. Т очная XML-схема отличается для каждого контейнера, но всеобщая структура остается аналогичной. П редупреждение Как только ваше приложение будет вырастать в размерах и усложняться, тоже самое будет происходить и с вашим конфигурационным файлом, если вы используете конфигурационную композицию. Он может стать настоящим камнем преткновения, поскольку этот файл моделирует такие сущности кода, как классы, параметры и тому подобное, но без преимуществ компилятора, опций отладки и т.д. Конфигурационные 94 файлы будут становиться хрупкими и непрозрачными с точки зрения наличия ошибок, поэтому используйте данный подход только, если вам необходимо позднее связывание. Из-за таких недостатков, как подробность и хрупкость, вам следует воспользоваться другими альтернативами для конфигурирования контейнеров. Т ехнология использования кода в качестве конфигурации схожа с XML конфигурацией по глубине детализации и по концепции, но очевидно использует код вместо XML. Конфигурирование контейнеров с помощью кода Возможно, самый легкий способ компоновки приложения – написать код, который выполняет компоновку. Может показаться, что такой подход идет в разрез со всей сущностью механизма внедрения зависимостей, потому что он жестко кодирует то, какие конкретные реализации должны использоваться для всех абстракций. Т ем не менее, если это выполняется в Com position Root, то оно изменяет только одно из преимуществ, перечисленных в таблице 1-1. Преимущество позднего связывания теряется, если зависимости жестко закодированы, но, как я уже упоминал в главе "Дегустационное меню" механизма внедрения зависимостей", это может и не иметь отношения ко всем типам приложений. Если ваше приложение разворачивается в ограниченное количество экземпляров в контролируемой среде, было бы проще заново скомпилировать и развернуть приложение, когда вам нужно заменить модули. Я част о думаю, чт о люди чересчур ревност но от носятся к определению конфигурационных файлов. Част о язык программирования является пря молинейным и мощным механизмом конфигурирования. Март ин Фаулер При использовании кода в качестве конфигурации мы явно устанавливаем такие же самые дискретные преобразования, которые мы устанавливали при использовании XML конфигурации – только мы используем код вместо XML. За единственным исключением в виде Spring.NET, все DI-контейнеры полностью поддерживают технологию использования кода в качестве конфигурации как альтернативы XML конфигурации – в действительности, в большинстве из них эта технология присутствует в виде используемого по умолчанию механизма, а XML конфигурация используется в качестве необязательной возможности. API, открытый для поддержки технологии использования кода в качестве конфигурации, отличается для каждого DI-контейнера, но всеобщей целью все еще остается определение дискретных преобразований между абстракциями и конкретными типами. П одсказка Используйте код в качестве конфигурации вместо XML конфигурации, за исключением тех моментов, когда вам нужно позднее связывание. Компилятор может быть полезным, а Visual Studio, которая формирует систему, будет автоматически копировать все необходимые зависимости в выходную папку. 95 Многие конфигурационные API используют дженерики и Fluent Builders для регистрации компонентов; StructureMap – не исключение. П ример: Конфигурирова ние шаблонного коммерческого приложения с помощью кода В разделе "Конфигурирование контейнеров при помощи XML" вы увидели, как сконфигурировать шаблонное коммерческое приложение с помощью XML, используя Unity. Я также мог бы продемонстрировать использование кода в качестве конфигурации на примере Unit y, но в этом примере я буду использовать StructureMap; так как он обладает более выразительным API, он лучше подходит для страниц этой книги. Используя конфигурационное API StructureMap, вы можете выразить конфигурацию из листинга 3-1 более компактно, как это продемонстрировано в следующем листинге. Листинг 3-2: Конфигурирование StructureMap с помощью кода c.For .Use .Use .ConnectionStrings["CommerceObjectContext"].ConnectionString; c.For .Ctor .Ctor ().Use .Ctor .Ctor Сравните этот код с кодом из листинга 3-1 и заметьте, насколько он более компактен – несмотря на то, что выполняет он тоже самое. Т акое простое преобразование, как преобразование IBasketService в BasketService , выражается с помощью видовых методов For и Use . Переменная c фактически является так называемым ConfigurationExpression , но воспринимайте ее как контейнер. Для того чтобы поддержат ь те классы, для которых нужна строка соединения, вы продолжаете последовательность For / Use путем вызова метода Ctor и передачи строки соединения. Метод Ctor выполняет поиск строкового параметра в конструкторе конкретного класса и использует переданное значение для этого параметра. Остальная часть кода повторяет эти два паттерна. Использование кода в качестве конфигурации не только компактнее XML конфигурации, но также поддерживается компилятором. Т ипы аргументов, используемые в листинге 3-2, представляют собой реальные типы, которые проверяет компилятор. Переменное API StructureMap поставляется даже с некоторыми видовыми ограничителями, которые сообщают компилятору о проверке того, совпадает ли тип, определяемый методом Use с абстракциями, обозначенными с помощью метода For . Если преобразование невозможно, то код не компилируется. Несмотря на то, что технология использования кода в качестве конфигурации безопасна и проста в применении, ее нужно больше сопровождать, нежели вам того хотелось. Каждый 96 раз при добавлении в приложение нового типа вы также должны помнить и о его регистрации – а многие регистрации похожи друг на друга. Автоматическая регистрация относится к этой теме. Конфигурирование контейнеров с помощью соглашений Обратили ли вы в листинге 3-2 внимание на то, насколько схожи большинство регистраций? В частности все компоненты доступа к данным, базирующиеся на SQL Server, руководствуют ся универсальным паттерном в тех случаях, когда вы конфигурируете компонент подходящей строкой соединения. Неоднократное написание такого кода регистрации нарушает принцип DRY (Don't Repeat Yourself – Не повторяйся). К тому же он выглядит как непродуктивный фрагмент кода инфраструктуры, который не добавляет особую значимость в приложение. Вы можете сэкономить время и допустить меньшее количество ошибок, если сможете автоматизировать процесс регистрации компонентов. Все более популярной становится архитектурная модель – "соглашения по конфигурации" (Convent ion over Configurat ion). Вместо того чтобы писать и поддерживать большие объемы конфигурационного кода, вы можете принять соглашения, которые влияют на базу кода. Способ, при помощи которого ASP.NET MVC находит контроллеры по их именам, – это отличный пример простого соглашения. 1. Поступает запрос контроллера с именем Home 2. Используемая по умолчанию фабрика контроллеров ищет в списке известных пространств имен класс с названием HomeController . Если она находит такой класс и этот класс реализует IController , то это как раз то, что нужно. 3. Используемая по умолчанию фабрика контроллеров использует конструктор по умолчанию найденного класса для того, чтобы создать экземпляр контроллера. Здесь используются, по крайней мере, два соглашения: контроллер должен иметь название [Имя контроллера]Controller и должен обладать конструктором по умолчанию. Вы можете отступить от этих соглашений посредством реализации своей собственной IControllerFactory , а это как раз то, что я и делал до настоящего времени, чтобы поддержать технологию внедрения через конструктор – более подробно я буду рассуждать об этом в главе "Построение объектов". Было бы здорово, если бы вам удалось использовать несколько соглашений для того, чтобы избавиться от всех этих приводящих к ошибкам и трудоемким конфигурациям контейнеров. Для DefaultControllerFactory добавлять новые контроллеры также просто, как и добавлять соответствующе названный класс в корректное пространство имен. Нам бы хотелось сохранить это соглашение даже при использовании технологии внедрения через конструктор. Многие DI-контейнеры предоставляют возможности автоматической регистрации, которые позволяют нам вводить свои собственные соглашения. О пределение 97 Автоматическая регистрация – это возможность автоматически регистрировать компоненты в контейнере путем поиска реализаций необходимых абстракций в одной или более одной сборке. Соглашения можно применять не только к контроллерам ASP.NET MVC. Чем больше соглашений вы добавляете, тем больше вы сможете автоматизировать различные составляющие конфигурации контейнеров. П одсказка Принцип "Соглашения по конфигурации" имеет больше преимуществ, нежели просто поддержка DI конфигурации. Он делает ваш код более последовательным, поскольку код будет автоматически работать, пока вы будете следовать вашим соглашениям. В действительности вам может понадобиться комбинировать автоматическую регистрацию с технологией использования кода в качестве конфигурации или XML конфигурацией, поскольку у вас может не получиться приспособить каждый единичный компонент к значимому соглашению. Т ем не менее чем больше вы сможете приспособить вашу базу кода к соглашениям, тем более поддерживаемым он будет. П ример: Конфигурирова ние шаблонного коммерческого приложения с помощью ме ханизма автоматической регистрации StructureMap поддерживает автоматическую регистрацию, но думаю, было бы более интереснее использовать все-таки другой DI-контейнер для конфигурирования шаблонного коммерческого приложения при помощи соглашений. Я выбрал Autofac, поскольку он обладает достаточно читабельным API автоматической регистрации. Если вы рассмотрите листинги 3-1 и 3-2, то надеюсь, что вы согласитесь с тем, что регистрирование различных компонентов доступа к данным – это наиболее повторяющиеся части кода. Можем ли мы выразить для них соглашение некоторого рода? Все четыре конкретных типа обладают следующими общими характеристиками: Все они определены в одной и той же сборке. Каждый из них является конкретным классом, унаследованным от абстрактного базового класса. Имя каждого из них начинается с Sql. Каждый из них обладает единственным открытым конструктором, который принимает строковый параметр с названием connString Кажется, будто соответствующее соглашение будет выражать эти сходства путем просмотра соответствующей сборки и регистрирования всех классов, которые соответствуют соглашению. Для Autofac это выглядело бы следующим образом: string connectionString = ConfigurationManager .ConnectionStrings["CommerceObjectContext"] .ConnectionString; var a = typeof(SqlProductRepository).Assembly; builder.RegisterAssemblyTypes(a) .Where(t => t.Name.StartsWith("Sql")) .As(t => t.BaseType) .WithParameter("connString", connectionString); 98 Это отдельное соглашение должно просматривать сборку, которая содержит компоненты доступа к данным. Существует несколько способов получения ссылки на эту сборку, но самый простой способ – подобрать характерный тип, например SqlProductRepository , и получить сборку из него. Вы могли бы выбрать и другой класс или найти сборку по имени. Т еперь, когда у вас есть сборка, вы можете сообщить контейнеру о том, что вы хотите просмотреть ее. Метод RegisterAssemblyTypes указывает на намерение регистрировать все типы сборки, которые соответствуют критерию, согласно которому имя класса должно начинаться Sql . Переменная builder – это экземпляр класса ContainerBuilder , но вы можете считать, что он является контейнером. Каждый из классов, которые выполняют это с помощью фильтра Where , должны быть зарегистрированы относительно их базового класса. Например, базовым классом SqlProductRepository является ProductRepository , его можно получить путем преобразования ProductRepository в SqlProductRepository В конце концов, вы утверждаете, что каждый конструктор обладает параметром connString и что его значение необходимо определить по строке соединения, считанной из конфигурационного файла. Сравнения этого соглашения с четырьмя регистрациями листинга 3-2 может быть недостаточно, поскольку мы также поддерживаем два других DI-контейнера. Вы все еще можете думать, что это преимущество выглядит незначительным. Т ем не менее, соглашение масштабирует намного лучше. Поскольку в текущем примере присутствует только четыре компонента доступа к данным, вы сохраняете всего несколько операторов с помощью соглашений. Несмотря на это, как только соглашение записано, оно управляет сотнями компонентов без особых усилий. Вы также можете обратиться к другим преобразованиям из листингов 3-1 и 3-2 посредством соглашений, но на данный момент от этого не будет никакой пользы. В качестве примера вы можете зарегистрировать все сервисы с помощью следующего соглашения: builder.RegisterAssemblyTypes(typeof(BasketService).Assembly) .Where(t => t.Name.EndsWith("Service")) .AsImplementedInterfaces(); Это соглашение просматривает определенну ю сборку на факт наличия типов, названия которых заканчиваются на Service , и регистрирует каждый тип относительно интерфейса, который он реализует. Фактически это соглашение регистрирует BasketService относительно интерфейса IBasketService , но поскольку в настоящее время у вас нет никаких других совпадений с этим соглашением, вы ничего из этого не получаете. Однако возможно имеет смысл сформулировать соглашение заранее для того, чтобы вдохновить разработчиков на следование этому соглашению. Автоматическая регистрация – это мощная технология, которая имеет все необходимое, чтобы сделать DI-контейнер невидимым. После размещения соглашений вам, возможно, понадобится изменять конфигурацию контейнера только в редких случаях. 99 До настоящего момента вы видели три разных подхода к конфигурированию DI- контейнера: XML Использование кода в качестве конфигурации Автоматическая регистрация Ни один из них не является взаимно исключающим. Вы можете выбрать вариант смешивания автоматической регистрации с определенными преобразованиями абстракций в конкретные типы, и даже смешать все три подхода, чтобы немножко выполнять автоматическую регистрацию, немножко использовать код в качестве конфигурации, а также иметь немного конфигурации в XML для целей позднего связывания. В качестве эмпирического правила вам следует использовать автоматическую регистрацию как стартовую точку, дополненную технологией использования кода в качестве конфигурации для управления более особыми случаями. Вам следует сохранить XML для тех случаев, когда вам нужно уметь изменять реализацию без повторной компиляции приложения (что намного реже, нежели вы думаете). Т еперь, когда мы узнали, как сконфигурировать DI-контейнер и как с помощью него преобразовывать диаграммы объектов, вы должны представлять, как это использовать. Использование DI-контейнера – это одно, а вот корректное его использование – это другое. 100 3.3. Паттерны DI-контейнеров DI-контейнеры – это мощные инструменты, но, как и для всех инструментов, существуют корректные и некорректные способы их использования. Т ак же, как и повара знают, что нужно бережно относиться к своим ножам, так и вы должны знать, что следует правильно обращаться с вашим DI-контейнером – он не может отрубить вам пальцы, но вы можете не получить те преимущества, которыми он обладает. Самое важное, что вам нужно понять – в каком месте архитектуры приложения следует использовать DI-контейнер. После того, как вы это поймете, вам еще нужно будет знать, каким образом его использовать. Следующие два мини-паттерна предоставляют ответы на эти вопросы. Composition Root В каком месте нам следует форм ироват ь диаграм мы объект ов? Как можно ближе к точке входа в приложение. DI-контейнер – это библиотека, которую вы можете использовать в тех местах, в которых только пожелаете – но это не означает, что вы должны его использовать. Несмотря на то, что вы можете расширить использование контейнера таким образом, чтобы он распространился на огромное количество ваших классов, вместо этого сосредоточьте его в единственной области своего приложения. Это место называется Composition Root , и вам следует использовать DI-контейнер только из этого места. Сущность Composition Root особо не связана с DI-контейнерами. Composition Root также применяется в тех случаях, когда вы используете Poor man's DI, но я думаю, что важно обсудить Composition Root именно в этом контексте, поскольку понимание этого паттерна позволяет вам использовать ваш DI-контейнер корректно и эффективно. Перед тем, как я приступлю к обсуждению роли Com position Root при использовании DI-контейнеров, я кратко, в общих чертах, рассмотрю его. Рисунок 3-6: При формировании приложения из множества слабо связанных классов композиция должна осуществляться как можно ближе к точке входа в приложение. Composition Root формирует диаграмму объектов, которая затем выполняет фактическую работу приложения. 101 Composition Root как основная сущность При написании слабо связанного кода вы создаете множество классов, которые вы должны скомпоновать для того, чтобы создать приложение. Компоновать незначительное количество этих классов в единицу времени может быть довольно заманчивой идеей для создания небольших подсистем, но такой подход ограничивает вашу способность перехватывать эти системы с целью модификации их поведения. Вместо этого вы должны компоновать все классы одновременно. О пределение Com position Root – это (предпочтительно) уникальное местоположение в приложении, где модули соединяются друг с другом. Подсказка Com position Root может разворачиваться в рамках составных классов, пока они располагаются в единственно м модуле. При изолированном рассмотрении Constructor Injection вас может заинтересовать такой вопрос: "не перемещает ли он решение о выборе зависимости всего лишь в другое место?". Да, это именно так, и это хорошо; это означает, что вы получаете центральное место, в котором можете соединять классы. Com position Root играет роль стороннего компонента, который связывает покупателей с их сервисами. В действительно сти, Нэт Прайс предпочитает термин "Стороннее соединение" вместо "механизм внедрения зависимостей" именно по этой причине. Чем дольше вы будете откладыват ь принятие решения о том, как соединять классы, тем дольше вы будете сохранять ваши опции открытыми. Т аким образом, Composition Root необходимо размещать как можно ближе к точке входа в приложение. Примечание Мне нравится думать о Com position Root как об архитектурном эквиваленте понятия Бережливой разработки программного обеспечения (Lean Soft ware Developm ent ): Последний ответственный момент. Смысл в том, чтобы откладывать все решения настолько долго, насколько это позволяют правила приличия (но не дольше), потому что 102 нам хотелось бы сохранять наши опции открытыми и основывать наши решения на как можно большем объеме информации. Когда дело доходит до компоновки приложений, мы можем таким же образом отложить принятие решения о передаче зависимостей в основание приложения. Даже у модульного приложения, которое для своего формирования использует слабое связывание и позднее связывание, есть основание, которое содержит точку входа в приложение. Ниже приведены примеры: Консольное приложение – исполняемый файл (.exe) с методом Main ASP.NET веб-приложение – это библиотека (.dll) с обработчиком событий Application_Start в файле Global.asax WPF приложение – исполняемый файл (.exe) с файлом App.xaml WCF сервис – это библиотека (.dll) с классом, который наследуется от интерфейса сервиса, несмотря на то, что вы можете заполучить более низкоуровневую точку входа путем создания пользовательско й ServiceHostFactory Существует множество других технологий, но общим для всех этих технологий является то, что один модуль содержит точку входа в приложение: это основание приложения, что проиллюстрировано на рисунке 3-7. Composition Root приложения должно располагаться в основании приложения таким образом, чтобы оно могло правильно формировать приложение. Рисунок 3-7: Точка входа в приложение – это основание модульного приложения. Либо напрямую, либо не напрямую основание использует остальные модули. Com position Root должно размещаться в основании приложения – как можно ближе к точке входа Вы не должны пытаться компоновать классы в любом модуле, потому что такой подход ограничивает ваши возможности. Все классы модулей приложения должны использовать Constructor Injection (или, в редких случаях, один из других паттернов, описанный в главе "DI-паттерны") и оставлять задачу формирования диаграммы объектов приложения за 103 Com position Root . Любой используемый DI-контейнер должен быть ограничен Com position Root . Использование DI-контейнера в Composition Root DI-контейнер может ошибочно использоваться в качестве Service Locator, но его можно использовать только как движок, который формирует диаграммы объектов. При рассмотрении DI-контейнера с этой точки зрения имеет смысл ограничить его Composition Root. Т акой подход также имеет большое преимущество, заключающееся в удалении любого связывания между DI-контейнером и остальной частью базы кода. Подсказка К DI-контейнеру следует обращаться из Com position Root. Все остальные модули не должны ссылаться на контейнер. На рисунке 3-8 вы можете увидеть, что только Composition Root ссылается на DI- контейнер. Остальная часть приложения не имеет ссылок на контейнер и вместо него полагается на паттерны, описанные в главе "DI-паттерны". DI-контейнеры понимают эти паттерны и используют их для того, чтобы формировать диаграмму объектов приложения. Рисунок 3-8: Только Composition Root , содержащееся в основании приложения, должно иметь ссылку на DI-контейнер. Все остальные модули приложения должны целиком полагаться на DI-контейнеры и не ссылаться на контейнер Com position Root может быть реализован с DI-контейнером. Это означает, что вы используете контейнер для формирования полноценной диаграммы объектов приложения в вызове единственного метода Resolve . Всякий раз, когда я говорю разработчикам о том, 104 чтобы они поступали именно так, я всегда могу сказать, что это доставляет им неудобства, поскольку они боятся, что это ужасно неэффективно и неудобно для выполнения. Вам не придется об этом беспокоиться, потому что это почти никогда не случается, и в тех немногих ситуациях, когда это происходит, существуют способы решения этой проблемы. П одсказка Не беспокойтесь об издержках выполнения, возникающих в ситуациях, когда DI- контейнер используется для формирования большой диаграммы объектов. Это почти никогда не является проблемой. Когда дело касается приложений, в основе которых лежат запросы, например веб-сайтов или сервисов, вы можете сконфигурировать контейнер всего один раз, но преобразовывать диаграмму объектов для каждого входящего запроса. Шаблонное коммерческое приложение является примером такой ситуации. П ример: Реализация Composition Root Шаблонное коммерческое приложение из раздела "Расширение шаблонного приложения" должно обладать Composition Root для того, чтобы формировать диаграммы объектов для входящих HTTP-запросов. Что касается всех остальных .NET веб-приложений, то для них точкой входа является метод Application_Start файла Global.asax В этом примере я использую DI-контейнер Castle Windsor, но код может быть таким же и для любого другого контейнера. Для Castle Windsor метод Application_Start мог бы выглядеть следующим образом: protected void Application_Start() { MvcApplication.RegisterRoutes(RouteTable.Routes); var container = new WindsorContainer(); container.Install(new CommerceWindsorInstaller()); var controllerFactory = new WindsorControllerFactory(container); ControllerBuilder.Current.SetControllerFactory(controllerFactory); } Перед тем как вы сможете сконфигурировать контейнер, вы должны создать новый экземпляр. Поскольку полноценная установка приложения инкапсулирована в классе под названием CommerceWindsorInstaller , вы инсталлируете его в контейнер для того, чтобы настроить этот контейнер. Код CommerceWindsorInstaller , очевидно, реализован с помощью API Castle W indsor, но концептуально он идентичен примеру из раздела "Конфигурирование DI-контейнеров". Для того чтобы позволить контейнеру подключать контроллеры в приложение, вы должны применить соответствующий шов ASP.NET MVC, который называется IControllerFactory (подробно рассматривается в разделе "Построение ASP.NET MVC приложений"). На данный момент достаточно понимать, что для интеграции с ASP.NET MVC вы должны создать адаптер в рамках контейнера и сообщить об этом фреймворку. Поскольку метод Application_Start выполняется всего лишь раз, контейнер является единственным экземпляро м, который инициализируется всего один раз. При получении 105 запросов этот экземпляр контейнера должен обрабатывать все запросы одновременно – но поскольку все контейнеры реализованы с помощью методов Resolve , корректно исполняемых в многопоточной среде, это не является проблемой. Поскольку вы устанавливаете ASP.NET MVC с пользовательско й WindsorControllerFactory , он будет вызывать ее метод GetControllerInstance для каждого входящего HTTP-запроса (подробнее об этом вы можете прочитать в разделе "Построение ASP.NET MVC приложений"). Реализация делегирует работу контейнеру: protected override IController GetControllerInstance( RequestContext requestContext, Type controllerType) { return (IController)this.container.Resolve(controllerType); } Обратите внимание на то, что вы более или менее вернулись к вводным примерам из раздела "Контейнер "Hello"". М етод Resolve формирует окончательную диаграмму, которая должна использоваться для этого конкретного запроса, и возвращает эту диаграмму. Это единственное место в приложении, где вы вызываете метод Resolve Подсказка База кода приложения должна содержать единственный вызов метода Resolve В этом примере Com position Root развернут в рамках нескольких классов, что продемонстрировано на рисунке 3-9. Это предсказуемо – важно, что все классы содержатся в одном и том же модуле, которым в данном случае является основание приложения. Рисунок 3-9: Com position Root развернут в рамках трех классов, но все они определены в пределах одного и того же модуля Самое важное, на что здесь нужно обратить внимание, – это то, что эти три класса являются единственными классами во всем шаблонном приложении, которые ссылаются на DI-контейнер. Вся остальная часть кода приложения использует только паттерн Construction Injection; вернитесь назад и перечитайте главу "Комплексный пример", если вы мне не верите. Подсказка Мне хотелось бы обобщить все правила, содержащиеся в данном разделе, перефразировав голливудский принцип: не вызывайте контейнер; он сам вызовет вас. 106 Comm on Service Locator Существует проект с открытым исходным кодом под названием Common Service Locator ( http://com monservicelocator.codeplex.com/ ), целью которого является отсоединение кода приложения от конкретного DI-контейнера путем скрытия каждого контейнера за универсальным интерфейсом IServiceLocator. Надеюсь, что такое объяснение того, как Com position Root эффективно отделяет остальную часть кода приложения от DI-контейнеров, теперь позволит вам понять, почему вам не нужен Comm on Service Locator. Как я буду объяснять в разделе "Service Locator", в связи с тем, что Service Locator является анти-паттерном, будет лучше не использовать его – тем более с Com position Root он вам и не нужен. Более подробно о том, как реализовывать Composition Root s в различных фреймворках (включая ASP.NET MVC), вы можете прочитать в главе "Построение объектов". В данном контексте то, как вы это делаете, более важно, чем то, где вы это делаете. Как и подразумевает его название, Composition Root – это составляющая основания приложения, в которой вы компонуете все слабо связанные классы. Это справедливо и при использовании DI-контейнера, и при использовании Poor Man's DI. Т ем не менее, при использовании DI-контейнера вам следует руководствоваться паттерном Register Resolve Release. Register Resolve Release Как нам следует использовать DI-контейнер? Следуя строгой последовательно сти вызовов методов Register Resolve Release Паттерн Composition Root описывает то, где вам следует использовать DI-контейнер. Т ем не менее, он не сообщает о том, как его использовать. Паттерн Register Resolve Release отвечает на этот вопрос. DI-контейнер следует использовать в трех, следующих друг за другом фазах называемых Register, Resolve и Release. В таблице 3-3 более подробно описана каждая из этих фаз. О пределение Паттерн Register Resolve Release указывает на то, что методы DI-контейнера должны вызываться в этой строгой последовательности: Регистрация (Register), Решение (Resolve) и Освобождение (Release) (см. рисунок 3-10). Рисунок 3-10: Методы DI-контейнера должны вызываться в продемонстрированной строгой последовательности: сначала метод Register, затем метод Resolve и в завершении метод Release |