Внедрение зависимостей в. Внедрение зависимостей в .NET. Руководство по применению этого механизма. Net приложениях. Книга демонстрирует основные паттерны на обычном языке C#, поэтому вы в полной мере поймете, как работает механизм внедрения зависимостей
Скачать 5.66 Mb.
|
. Иронично, не так ли? Вы можете подумать, что добавление дополнительной ссылки к проекту модульного тестирования – это не так плохо, но это имеет больше недостатков, которые не сразу становятся очевидными. Косвенны е зависимости Подробное рассмотрение того, почему проект юнит тестов должен ссылаться только на тот проект, на который он направлен, выходит за рамки этой книги, но в целом проблема 226 состоит в том, что юнит тесты создают косвенную зависимость между PresentationModel и SqlDataAccess . Хотя оба эти проекты могут существовать и быть скомпилированы отдельно, проект модульного тестирования связывает их вместе. Эта косвенная зависимость может быть сломана только тогда, когда будет исключен модульный тест, который изначально ее создал. Т ем не менее, юнит тесты написаны для того, чтобы выполняться, так что это далеко от желаемого. Если мы хотим оставить такие юнит тесты, предотвращающие тесную связанность, в рамках существующего проекта модульного тестирования, цена добавления жесткой ссылки на все нежелательные сборки слишком велика. Лучше всего определять нежелательные зависимости при помощи строк, как показано в листинге 6-4. Недостатком является то, что если мы изменим название запрещенной сборки, тест становится бесполезным или, возможно, еще хуже – мы будем думать, что мы защищены, а на самом деле это будет не так. Это не проблема, если у нас есть основания полагать, что имена сборок стабильны. Если же это не так, нам нужна другая стратегия. Связывание при интеграционном тестировании Существуют веские причины того, почему проекты юнит тестов должны ссылаться только на свои цели. Оставаясь надежными в условиях изменения имен сборок, нам все же иногда могут понадобиться безопасные для типов ссылки на все нежелательные зависимости. Эти звучит как противоречивое требование, но мы можем решить эту головоломку, введя новый проект по интеграционному тестированию. Вы можете добавить новый тестовый проект в решение Com m erce (Comm erce solution) и добавить все нужные ссылки. Рисунок 6-18 показывает это решение, и хотя он очень похож на рисунок 6-17, разница состоит в том, что для интеграционного теста все ссылки легальны и одинаково действительны. Рисунок 6-18: Проект CommerceIntegrationTest содержит автоматизированные тесты, которые проверяют, что отношения между модулями являются правильными. В отличие от модульных тестов, интеграционные тесты могут содержать столько ссылок, сколько необходимо, для проведения данного теста. 227 И нте грационны е тесты Интеграционное т ест ирование – это еще один тип автоматизированного тестирования на уровне API. Разница между модульным тестом и интеграционным тестом заключается в том, что модульное тестирование имеет дело с модулями (юнитами) в изоляции, в то время как интеграционные тесты сосредоточены на проверке того, что несколько юнитов (часто в разных библиотеках) интегрируются друг с другом, как нужно. Согласно определению, проект по интеграционному тестированию может ссылаться на все зависимости, которые ему нужны, чтобы делать свою работу, так что это хорошо подходит для тестов, соблюдающих архитектурные ограничения. Набор интеграционных тестов тесно связан с конкретной совокупностью модулей, так что он менее многоразовый. Он должен содержать только тесты, которые абсолютно могут быть определены как интеграционные тесты, и тесты, которые защищают от нежелательно й связанности, принадлежат к этой категории. Листинг 6-5 показывает безопасный для типов тест, эквивалентный тесту из листинга 6-4. Он следует тому же плану, но меняется, когда дело доходит до выявления нежелательно й зависимости. Листинг 6-5: Обеспечение слабой связанности при помощи интеграционного теста 1. [Fact] 2. public void PresentationModuleShouldNotReferenceSqlDataAccess() 3. { 4. // Fixture setup 5. Type presentationRepresentative = 6. typeof(HomeController); 7. Type sqlRepresentative = 8. typeof(SqlProductRepository); 9. // Exercise system 10. var references = 11. presentationRepresentative.Assembly 12. .GetReferencedAssemblies(); 13. // Verify outcome 14. AssemblyName sqlAssemblyName = 15. sqlRepresentative.Assembly.GetName(); 16. AssemblyName presentationAssemblyName = 17. presentationRepresentative.Assembly.GetName(); 18. Assert.False(references.Any(a => 19. AssemblyName.ReferenceMatchesDefinition( 20. sqlAssemblyName, a)), 21. string.Format( 22. "{0} should not be referenced by {1}", 23. sqlAssemblyName, 24. presentationAssemblyName)); 25. // Teardown 26. } Строки 10-12: Получить сборки, на которые ссылаются Строки 14-17: Получить имена сборок Строки 18-20: Искать нежелательную зависимость Т еперь, когда у вас есть ссылки на все необходимые зависимости, вы можете выбрать тип из каждого модуля, который можно использовать, чтобы представить их сборки. В отличие от предыдущего примера, это можно определить безопасным для типа способом. 228 Т ак же как и раньше, вы получите список всех сборок, на которые ссылается библиотека PresentationLogic . Используя AssemblyName каждой сборки, вы проверяете, чтобы ссылки не содержали сборку на основе SQL Server. Встроенные статический метод ReferenceMatchesDefinition сравнивает имена сборок. Возможно, вы заметили, что тесты в листингах 6-4 и 6-5 аналогичны. Вы могли бы написать новые тесты, как тот, что в листинге 6-5, изменив два репрезентативных типа и оставив все остальное, как есть. Следующим логическим шагом было бы выделение общей части теста в параметризованный тест (Paramet erized Test). Это позволит вам написать простой список почти декларативных тестов, которые определяют, что разрешено или не разрешено в этой конкретной совокупности модулей. Модульные тесты и интеграционные тесты являются отличными вариантами, если вы уже используете автоматизированные тесты на API уровня. Если же нет, вы должны начать делать это как можно скорее, но есть и другие альтернативы. Использование NDepend для мониторинга связанности Если по каким-то непостижимым причинам вы не хотите использовать юнит тесты, вы можете использовать инструмент под названием NDepend ( ht tp://ndepend.com ), который предупредит вас, если вы или члены вашей команды ввели нежелательну ю связанность. NDepend является коммерческим инструментом программного обеспечения, который анализирует проекты или решения и дает много статистики о коде. В качестве примера, он может генерировать графы зависимостей, которые мало чем отличаются от тех, что вы видели в этой книге. Если проанализировать коммерческое решение Мэри из главы 2, мы получим граф, показанный на рисунке 6-19. Рисунок 6-19: Граф зависимостей, сгенерированный NDepend, для коммерческого решения Мэри. По умолчанию NDepend включает все зависимости, в том числе модули из BCL. Размер блоков отображает число строк кода в каждом модуле, а толщина стрелки отражает число элементов, используемых по всем ссылкам. Это кажется сложным, но мы можем скрыть BCL модули, и мы получим рисунок 6-20. 229 Рисунок 6-20: Измененный график NDepend для коммерческого решения Мэри. На этом графе я вручную удалил все BCL модули и сделал блоки и стрелки одного размера. Выглядит ли рисунок 6-20 знакомыми? Если вы обладаете эйдетической памятью, вы можете вспомнить рисунок 2-10, в ином же случае просто вернитесь к нему. Обратите внимание, что они представляют одни и те же структуры и иллюстрируют одни и те же отношения. NDepend может сделать гораздо больше, чем нарисовать симпатичные графики. Одна из его наиболее мощных возможностей – это Code Query Language (CQL), который позволяет нам запрашиват ь наш код по широкому спектру информации и имеет синтаксис, напоминающий синтаксис SQL. Если бы Мэри написала CQL проверку, прежде чем разрабатывать решение, она была бы предупреждена до того, как был нанесен большой ущерб. Вот запрос, который мог бы избавить ее от многих неприятностей: WARN IF Count > 0 IN SELECT ASSEMBLIES WHERE IsDirectlyUsing "ASSEMBLY:Ploeh.Samples.Mary.ECommerce.Data.Sql" AND NameIs "Ploeh.Samples.Mary.ECommerce.Domain" После выполнения этот CQL запрос выдает предупреждение, если доменный модуль напрямую обращается к модулю SQL Server DataAccess. В решении Мэри этот запрос действительно выдаст предупреждение. 230 Мы можем написать столько CQL запросов для решения, сколько захотим, и запустить их либо с помощью редактора или автоматизировать процесс с помощью командной строки. В обоих случаях генерируются XML файлы с результатами анализа, поэтому мы можем написать свои собственные инструменты автоматизации, которые принимают соответствующие меры, если мы хотим включить такой шаг в автоматизированный процесс сборки. П римечание Я лишь поверхностно рассмотрел возможности NDepend. Он может делать много других вещей, но я хотел сосредоточиться на его способности следить за связанностью. NDepend и автоматизированные тесты – это два способа автоматического контроля кода, чтобы незаконные зависимости случайно не прокрались. Мы можем использовать один или оба из этих вариантов в рамках тестирования сборки (Build Verification T est, BVT ) или непрерывной интеграции (Continuous Integration, CI). Для больших баз кода, которые поддерживается большими командами, это может защитить нас от значительных проблем. Хотя мы не можем следить за всем, что происходит, и выполняем проверки кода вручную на каждом этапе, автоматизированные инструменты могут предупредить нас, если произойдет что-то странное. Внимание Некоторые инструменты могут вам «наврать», так что не стоит слепо верить им, когда они говорят вам, что есть проблемы. Всегда используйте ваш опыт и знания для оценки предупреждений. Откажись от них, если вы не согласны. Обращайте внимание на каждый случай, и если есть реальная проблема, решайте ее. Рассмотрите возможность использования автоматизированных инструментов для мониторинга связанности в больших базах кода. Это может предотвратить случайную тесную связанность и «порчу» вашего кода, а сами сосредоточьтесь на другие проблемах, описанных в этой главе. 231 6.6. Резюме DI не представляет собой особой сложности, если вы понимаете несколько основных принципов, но как вы поняли, вы почти наверняка столкнетесь с проблемами, которые могут поставить в тупик на некоторое время. В этой главе мы сделали попытку рассмотреть некоторые из наиболее распространенных проблем. Один из самых универсальных и полезных шаблонов проектирования, связанных с DI, является абстрактная фабрика. Мы можем использовать ее для перевода простых значений времени выполнения, таких как строки или числа, введенных пользователями, в экземпляр ы сложных абстракций. Мы также можем использовать абстрактные фабрики в сочетании с интерфейсом IDisposable , чтобы имитировать краткосрочные зависимости, такие как соединения с внешними ресурсами. С овет Переводите значения времени выполнения в зависимости при помощи абстрактных фабрик. С овет Имитируйте соединения при помощи абстрактных фабрик, которые создают одноразовые зависимости. Проблема, которая иногда возникает, – это циклы зависимостей. Они, как правило, появляются из-за того, что API-интерфейсы являются слишком строгими. Чем больше API строится вокруг парадигмы запросов, тем больше шансов получить циклы. Мы можем избежать циклов, соблюдая Принцип Голливуда (Hollywood Principle). Методы с void сигнатурами могут быть преобразованы в события, которые часто могут быть использованы для прерывания циклов. Если перепроектирование невозможно, мы можем разорвать цикл, изменив одно внедрение в конструктор во внедрение в свойство. Т ем не менее, это надо делать аккуратно, потому что это меняет семантику потребителя. С овет Ломайте циклы при помощи внедрения в свойство. Внедрение в конструктор должно быть вашим предпочтительным DI паттерном; дополнительным преимуществом является то, что всегда становится очевидно, когда вы нарушаете принцип единственной обязанности. Когда один класс имеет слишком много зависимостей, это сигнал, что мы должны его перестроить. Возможно, мы можем разбить его на несколько меньших классов, но иногда мы должны сохранить все функциональные возможности в рамках одного класса. С овет Решайте проблему Constructor Over-injection путем рефакторинга к фасадным сервисам. В этих случаях мы можем поднять уровень абстракции, вставив слой фасадных сервисов между потребителем и зависимостями. Выполнение такого рефакторинга часто приводит к положительно му побочному эффекту, что некоторые из этих фасадных сервисов 232 оказываются ранее неизвестными, неявными доменными концепциями. Выявление неявных концепций и вывод их в явные концепции является усовершенствованием доменной модели. В то время как мы выполняем этот мелкий рефакторинг, мы не должны упускать из виду общую картину. Автоматизированные тесты или инструменты могут помочь нам уследить за тем, если тесная связанность появляется в части кода. Если мы напишем много юнит тестов (в частности, если мы используем Test-Driven Developm ent ), тесная связанность быстро проявит себя в сложном и хрупком тестовом коде. Или, вероятно, потому что это невозможно – провести модульное тестирование больших частей приложения. С овет Пишите автоматизированные тесты для обеспечения слабой связанности. Если мы не будем писать юнит тесты, тесную связанность можно не заметить, но мы увидим многие из ее симптомов: когда код усложняется, его все сложнее и сложнее поддерживать. Хороший и чистый дизайн, к которому мы изначально стремились, медленно превращается в спагетти-код. Добавление новой функциональной возможности требует от нас того, чтобы мы залезли даже в несвязанные области. В этой главе описаны решения для часто встречающихся проблем с DI. Вместе с двумя предыдущими главами она образует каталог паттернов, анти-паттернов и рефакторинга. Данный каталог представляет собой часть 2 книги. В части 3 мы повернемся лицом к трем измерениям DI: Ком позиции объект ов (Object Com position), Управлению жизненным циклом (Lifetim e Managem ent) и Перехват у (Int erception). 233 DI самостоятельно В главе 1 "Дегустационное меню" механизма внедрения зависимостей" я предоставил краткий обзор трех аспектов механизма внедрения зависимостей: композиция объектов, управление жизненным циклом и механизм перехвата. В этой части книги я раскрою эту точку зрения в трех отдельных главах. Многие DI-контейнеры обладают чертами, которые напрямую относятся к этим аспектам. Некоторые DI-контейнеры предоставляют черты всех трех аспектов, тогда как остальные поддерживают только некоторые из них. Т ем не менее, поскольку DI-контейнер является необязательным инструментом, я считаю, что важнее объяснить основные принципы и приемы, которые контейнеры обычно используют для реализации этих характеристик. В части 2 "DI самостоятельно" исследуется то, как вы можете сделать это самостоятельно, а не с помощью DI- контейнера. Теоретически вы можете использовать эту информацию для создания своего собственного DI-контейнера (но, пожалуйста, не делайте это – миру не нужен еще один DI-контейнер), или применять же механизм внедрения зависимостей вообще без использования контейнера – это то, что мы называем Poor m an's DI. Однако главная цель этой части книги – изложить основные алгоритмы композиции объектов, управления жизненным циклом и механизма перехвата без необходимости использования конкретного DI-контейнера. Думаю, если бы я использовал конкретный контейнер, то было бы трудно отделить основные принципы от деталей конкретного API. Глава 7 "Построение объектов" объясняет, как компоновать объекты в таких различных фреймворках, как ASP.NET MVC, W PF, WCF и т.д. Не все фреймворки в равной степени хорошо поддерживают механизм внедрения зависимостей, и даже среди тех, которые его поддерживают, способы, с помощью которых они это выполняют, значительно различаются. Для каждого фреймворка трудно идентифицировать шов, который разрешает механизм внедрения зависимостей в этом фреймворке. Т ем не менее, при обнаружении этого шва вы получаете решение для всех приложений, которые используют этот конкретный фреймворк. В главе 7 я выполнил эту задачу для большинства общеизвестных фреймворков .NET приложений. Рассматривайте эту главу как каталог швов для фреймворков. Даже если выбранный вами конкретный фреймворк не рассматривается здесь, я попытался обратиться ко всем возможным видам ограничений фреймворков, с которыми вы можете столкнуться. Например, с точки зрения механизма внедрения зависимостей, PowerShell – это самый ограниченный тип фреймворка, о котором я только мог подумать, поэтому я использовал его в качестве примера. Вы должны уметь экстраполировать решение для схожих фреймворков, даже если они явно не рассматриваются. Несмотря на то, что компоновка объектов не усложняется благодаря Poor man's DI, вы должны начать понимать преимущества настоящего DI-контейнера после того, как прочтете о таком аспекте, как управление жизненным циклом. Существует возможность надлежащим образом управлять жизненным циклом различных объектов с помощью диаграммы объектов, но для этого нужно больше пользовательского кода, нежели для композиции объектов, и ни один из этих кодов не добавляет в приложение никакого конкретного бизнес-значения. Кроме объяснения основных принципов управления жизненным циклом, глава "Жизненный цикл объектов" также содержит каталог универсальных стилей существования. Этот каталог выступает в роли словаря для обсуждения стилей 234 существования на протяжении всей части "DI-контейнеры", поэтому, несмотря на то, что вам не нужно уметь реализовывать любой из этих стилей вручную, хорошо бы знать, как они работают. В главе "Механизм перехвата" мы рассматриваем часто встречающуюся проблему реализации сквозных сущностей на основе компонентов. Переходя окончательно от простого приложения паттерна проектирования Decorator к механизму перехвата в рабочей среде, мы рассматриваем способы формирования слабо связанных приложений в модульном исполнении. Я считаю эту главу кульминацион ной точкой книги – это то место, в котором многие читатели во время начальной программы по управлению доступом высказали мнение, что они начали понимать очертания чрезвычайно мощного способа моделирования программного обеспечения. Несмотря на то, что я использую Poor m an's DI для исследования и объяснения механизма внедрения зависимостей, я не рекомендую использовать его для профессионального использования. Многие отличные DI-контейнеры доступны на .NET и все они являются бесплатными. Т аким образом, часть "DI-контейнеры" посвящена подробному рассмотрению API конкретного контейнера. 7. Построение объектов 8. Жизненный цикл объектов 9. Механизм перехвата 235 7. Построение объектов Меню: Консольные приложения ASP.NET MVC Windows Com munication Foundation Windows Presentation Foundation ASP.NET (Web Form s) PowerShell Приготовление изысканных блюд из нескольких слоев – смелое начинание, особенно, если вы хотите поучаствовать в поедании этих блюд. Вы не можете хорошо кушать и готовить в одно и то же время, кроме того, многие блюда, чтобы они хорошо получились, необходимо готовить в последнюю минуту. Профессиональные кулинары знают, как решить большинство из этих задач. Среди многих хитростей они используют в качестве основного принцип "mise en place", который можно приблизительно перевести как "все готово": все, что может быть приготовлено хорошо заранее готовится заранее. Чистятся и нарезаются овощи, нарезается мясо, подготавливается инвентарь, разогревается духовка, раскладываются инструменты и т.д. Подготавливается настолько много компонентов, насколько это возможно. Если мороженое является частью десерта, то его можно сделать за день до приготовления десерта. Если первый слой содержит мидии, их можно почистить за несколько часов до этого. Даже такой недолговечный компонент, как беарнский соус можно приготовить за час до приготовления основного блюда. Когда гости готовы приступить к еде, необходимы только финальные приготовления: подогреть соус во время жарки мяса и т.д. Во многих случаях эта финальная композиция блюда не занимает более 5-10 минут. Рисунок 7-1 иллюстрирует этот процесс. Рисунок 7-1: Принцип "mise en place" подразумевает приготовление всех компонентов блюда заранее для того, чтобы финальная композиция блюда была выполнена настолько быстро и легко, насколько это возможно. Принцип "mise en place" похож на разработку слабо связанного приложения с помощью механизма внедрения зависимостей. Мы можем написать все требуемые компоненты заранее и компоновать их только тогда, когда мы должны это делать. Примечание В разделе "Composition Root " я сравнивал Composition Root с таким понятием Бережливой разработки программного обеспечения (Lean Software Development), как Последний от вет ственный момент . Сравнение Com position Root с принципом "mise en place" является схожей аналогией, несмотря на то, что при таком сравнении придается большое значение несколько другому аспекту: композиции. 236 Как и все аналогии, мы можем использовать их только до настоящего момента. Разница в том, что в кулинарии этапы подготовки и композиции разделены во времени, тогда как в разработке приложений это разделение проявляется в рамках модулей и уровней. Рисунок 7-2 демонстрирует то, как мы компонуем компоненты в Composition Root (часто на уровне пользовательского интерфейса). Рисунок 7-2: Com position Root соединяет все независимые модули приложения. В противоположность "mise en place" это не происходит настолько поздно, насколько это возможно, но происходит там, где необходима интеграция различных модулей. Первое, что происходит на этапе выполнения – это композиция объектов. Как только диаграмма объектов подключена, завершается композиция объектов и составные компоненты соединяются. Несмотря на то, что композиция объектов – это фундаментальная основа механизма внедрения зависимостей, она является самой простой для понимания составляющей. Вы уже знаете, как это делать, поскольку вы все время компонуете объекты при создании объектов, которые содержат в себе другие объекты. В разделе 3.3. "Паттерны DI- контейнеров" мы рассмотрели основные принципы того, когда и как компоновать приложения. Следовательно, я не собираюсь использовать следующие 40 страниц для того, чтобы поведать вам о том, как компоновать объекты. Вместо этого я хочу помочь вам обратиться к тем трудностям, которые могут возникнуть, когда вы будете компоновать объекты. Эти трудности возникают не из-за самой композиции объектов, а из-за фреймворков приложений, в которых вы хотели бы компоновать ваши объекты. Эти проблемы специфичны для каждого фреймворка и то же самое касается и резолюций. В моем случае эти проблемы принимают вид некоторых самых значимых препятствий для успешного применения механизма внедрения зависимостей, поэтому я сконцентрирую свое внимание именно на них. Это делает главу менее теоретической и более практической по сравнению с предыдущими главами. 237 Подсказка Если вы хотите почитать только о применении механизма внедрения зависимостей в выбранном вами фреймворке, то вы можете пропустить этот раздел. Каждый раздел является автономным. Легко формировать целостную иерархию зависимостей приложения, когда мы имеем полный контроль над жизненным циклом приложения (как мы поступаем с приложениями командной строки). Т ем не менее, большинство фреймворков (ASP.NET, WCF и т.д.) в .NET включают в себя инверсию управления, которая иногда может усложнять процесс применения механизма внедрения зависимостей. Понимание швов каждого фреймворка является ключевым моментом для применения механизма внедрения зависимостей к конкретному фреймворку. Как иллюстрирует рисунок 7-3, в этой главе мы будем рассматривать то, как реализовать Com position Root'ы в некоторых универсальных фреймворках стандартной библиотеки классов (BCL). Рисунок 7-3: Структура этой главы принимает форму каталога различных BCL фреймворков и швов, которыми они могут обладать для разрешения механизма внедрения зависимостей. Каждый раздел написан таким образом, что его можно читать независимо от остальных разделов. Примечание В связи с пространственными ограничениями я не буду рассматривать приложения Windows Form s. Т ем не менее, когда дело касается композиции объектов, то в этом они схожи с W PF приложениями. Каждый раздел посвящен одному из этих фреймворков и может быть прочитан более или менее независимо от остальных. Я буду начинать каждый раздел с общего введения к применению механизма внедрения зависимостей в этом конкретном фреймворке, затем последует обширный пример, который построен на универсальном примере Com merce, который используется в большей части данной книги. Начнем мы с самого простого фреймворка, в котором применяется механизм внедрения зависимостей, и постепенно перейдем к более сложным фреймворкам. Когда мы дойдем 238 до ASP.NET , нам нужно будет пересечь бездну, за пределами которой мы можем применять механизм внедрения зависимостей, только подвергая риску, по крайней мере, несколько наших принципов. Не опровергается тот факт, что такие фреймворки, как ASP.NET и PowerShell являются попросту враждебн ыми средами для применения механизма внедрения зависимостей, поэтому мы должны сделать все так хорошо, насколько мы это можем. Тем не менее, пока мы не достигнем этой точки, нет необходимости в компромиссе. Консольное приложение, вероятно, является самым простым типом приложения, в котором применяется механизм внедрения зависимостей. 7.1. Построение консольных приложений 7.2. Построение ASP.NET MVC приложений 7.3. Построение WCF приложений 7.4. Построение WPF приложений 7.5. Построение ASP.NET приложений 7.6. Построение PowerShell cm dlets 7.7. Р езюме 239 7.1. Построение консольных приложений Консольное приложение – это самый простой для композиции вид приложения. В противоположност ь большинству других фреймворков BCL приложений, консольное приложение, в сущности, не содержит инверсию управления. Когда процесс выполнения доходит до точки входа в приложение (обычно это метод Main ), мы берем все в свои руки. Не существует никаких особенных событий, которые нужно было бы описать, интерфейсов, которые нужно было бы реализовать, и мы можем использовать очень ограниченное количество сервисов. Метод Main соответствует Composition Root . Первое, что нам нужно сделать в методе Main , – скомпоновать модули приложения и позволить им соединиться в единое целое. Это не сложно, но давайте рассмотрим пример. Пример: актуализация валют В главе 4 "DI паттерны" мы рассматривали то, как обеспечить возможность конвертации валют в шаблонном приложении Comm erce. В разделе "Пример: конвертация валюты в корзине" был введен класс Currency , который предоставляет курс обмена одной валюты на другие. Поскольку Currency является абстрактным классом, мы могли бы создать множество различных реализаций, но в данном примере мы использовали базу данных. Целью примера кода из главы "DI паттерны" было продемонстрировать то, как восстановить и реализовать конвертацию валюты, таким образом, мы никогда не рассматривали то, как актуализировать обменный курс в базе данных. Для того чтобы продолжить пример, давайте изучим то, как написать простое консольное приложение, которое позволяет администратору или привилегированному пользователю обновлять обменные курсы в базе данных без необходимости прямого взаимодействия с базой данных. П рограмма Update Currency Поскольку целью этой программы является актуализация обменных курсов в базе данных, она имеет название UpdateCurrency.e xe. Она будет принимать в качестве входных данных три аргумента командной строки: Код целевой валюты Код исходной валюты Обменный курс Может показаться странным, что мы указываем цель до исходных данных, но такой способ является наиболее подходящим для большинства людей. Это говорит вам о том, сколько исходной валюты вам понадобится для того, чтобы купить одну единицу целевой валюты; например, обменный курс доллара к евро выражается как 1 евро за 1,44 доллара. В командной строке это выглядит примерно так: PS Ploeh:\> .\UpdateCurrency.exe EUR USD "1,44" Updated: 1 EUR in USD = 1,44. 240 В результате выполнения программы происходит обновление базы данных и запись обновленных значений обратно в консоль. Composition Root UpdateCurrency использует точку входа по умолчанию для консольного приложения: метод Main в классе Program . Это Composition Root для приложения, что продемонстрировано в следующем листинге. Листинг 7-1: Composition Root консольного приложения public static void Main(string[] args) { var container = new CurrencyContainer(); container.ResolveCurrencyParser() .Parse(args) .Execute(); } Единственной ответственностью метода Main является компоновка всех соответствующих модулей, и затем он передает ответственность за функциональнос ть сформированной диаграмме объектов. В данном примере пользовательский контейнер инкапсулирует то, как компонуются модули. Поскольку он выполняет точно такую же функцию, как и DI- контейнер, я решил назвать его конт ейнером, несмотря на то, что он является пользовательским контейнером с жестко подключенными зависимостями. Мы вернемся к нему для того, чтобы вкратце рассмотреть, как он реализован. После размещения контейнера вы можете теперь попросить его разрешить CurrencyParser , который анализирует входные аргументы и, в итоге, выполняет соотвествующу ю команду. П одсказка Composition Root должна делать всего две вещи: настраивать контейнер и разрешать тип, который реализует требуемую функциональность. Как только он это выполнил, он должен сойти с дистанции и оставить остальную работу разрешенному экземпляру. П одсказка Используйте для ваших рабочих приложений настоящий DI-контейнер вместо доморощенного пользовательского контейнера. В этом примере используется пользовательский контейнер, явно созданный для этого приложения, но необходимо прямо заменить его таким настоящим DI-контейнером, как те, что рассматривались в главе "DI паттерны". Контейнер Класс CurrencyContainer – пользовательский контейнер, созданный специально для целей подключения всех зависимостей к программе UpdateCurrency. Следующий листинг демонстрирует реализацию. 241 Листинг 7-2: Пользовательский CurrencyContainer 1. public class CurrencyContainer 2. { 3. public CurrencyParser ResolveCurrencyParser() 4. { 5. string connectionString = 6. ConfigurationManager.ConnectionStrings 7. ["CommerceObjectContext"].ConnectionString; 8. CurrencyProvider provider = 9. new SqlCurrencyProvider(connectionString); 10. return new CurrencyParser(provider); 11. } 12. } 13. Строка 5-7: Получает строку соединения из config В этом примере диаграмма зависимостей довольно поверхностная. Для класса CurrencyParser необходим экземпляр абстрактного класса CurrencyProvider , а в CurrencyContainer вы решаете, что реализацией должен быть SqlCurrencyProvider , который предоставляет необходимое взаимодействие с базой данных. Класс CurrencyParser использует механизм внедрения через конструктор, поэтому вы передаете в него только что созданный экземпляр SqlCurrencyProvider до того, как вернуть его из метода. Если вдруг вам станет интересно, то ниже я привожу сигнатуру конструктора CurrencyParser : public CurrencyParser(CurrencyProvider currencyProvider) Помните о том, что CurrencyProvider – это абстрактный класс, который реализуется SqlCurrencyProvider . Несмотря на то, что CurrencyContainer содержит жестко закодированное преобразование CurrencyProvider в SqlCurrencyProvider , остальная часть кода является слабо связанной, поскольку в ней применяется только абстракция. Этот пример может показаться простым, но в нем компонуются типы из трех различных уровней приложения. Давайте вкратце исследуем то, как эти уровни взаимодействуют в данном конкретном примере. Иерархическо е представление Composition Root – это место, в котором компоненты всех уровней соединяются вместе. Т очка входа и Com position Root образуют единственный исполняемый код. Вся реализация делегирована более низшим уровням, как это иллюстрирует рисунок 7-4. Рисунок 7-4: Композиция компонентов приложения UpdateCurrency. CurrencyParser анализирует аргументы командной строки и возвращает соответствующий ICommand . Если аргументы были понятны, то он возвращает CurrencyUpdateCommand , который использует экземпляр Currency для актуализации обменного курса. Вертикальная линия справа показывает соответствующий уровень приложения. Каждый уровень реализуется в отдельной сборке. 242 Диаграмма на рисунке 7-4 может казаться сложной, но она представляет почти всю базу кода приложения. Большая часть логики приложения состоит из анализа входных аргументов и выбора корректной команды на основании этих входных данных. Все это имеет место на уровне Application Services (сервисы приложения), который взаимодействует напрямую с уровнем доменной модели посредством абстрактных классов CurrencyProvider и Currency CurrencyProvider внедряется в CurrencyParser с помощью контейнера и впоследствии используется в качестве абстрактной фабрики для создания экземпляра Currency , который используется CurrencyUpdateCommand Уровень доступа к данным поставляет SQL Server реализации доменных классов. Несмотря на то, что никакие другие классы приложения не обращаются напрямую к этим классам, CurrencyContainer преобразует абстракции в конкретные классы. Использовать механизм внедрения зависимостей в консольном приложении легко, поскольку в действительности в нем нет внешней инверсии зависимостей. .NET Framework просто ускоряет процесс и передает управление методу Main В большинстве других BCL фреймворков присутствует более высокая степень инверсии управления, которая подразумевает, что нам нужно уметь определять корректные места расширяемости для того, чтобы подключить требуемую диаграмму объектов. Одним из таких фреймворков является ASP.NET MVC. 243 7.2. Построение ASP.NET MVC приложений ASP.NET MVC был создан с выраженным стремлением стать DI-дружественным, и именно таким он и является. Он не вынуждает использовать механизм внедрения зависимостей, но с легкостью разрешает его, не заставляя нас при этом делать предположения о том, какой вид механизма внедрения зависимостей мы будем применять. Мы можем использовать Poor's Man DI или такой DI-контейнер, какой только пожелаем. Расширяемость ASP.NET MVC Как всегда и происходит с механизмом внедрения зависимостей, ключ к его применению заключается в обнаружении корректных мест расширяемости. В ASP.NET MVC таким ключом является интерфейс под названием IControllerFactory . Рисунок 7-5 иллюстрирует то, как он вписывается в фреймворк. Рисунок 7-5: Когда рабочая среда ASP.NET MVC получает запрос, он просит свою фабрику контроллеров создать Controller для запрашиваемого URL. Фабрика контроллеров определяет корректный тип контроллера, который используется для данного запроса, создает и возвращает новый экземпляр этого типа. Затем ASP.NET MVC вызывает соответствующий метод действия для экземпляра Controller . После создания экземпляра контроллера ASP.NET MVC дает фабрике контроллеров освободить ресурсы путем вызова ReleaseController 244 Контроллеры являются центральным понятием в ASP.NET MVC. Они управляют запросами и определяют то, как на них откликаться. Если нам нужно сделать запрос к базе данных, проверить и сохранить входные данные, вызвать доменную логику и т.д., то мы инициируем такие действия из контроллера. Контроллер не должен выполнять такие действия сам, а должен делегировать работу соответствующим зависимостям. Это именно то место, где в игру вступает механизм внедрения зависимостей. Мы хотим уметь снабжать зависимостями данный класс Controller , в идеале с помощью внедрения через конструктор. Это можно сделать при помощи пользовательского IControllerFactory IDependencyResolver После выхода в 2011 году ASP.NET MVC 3 одной из новых реализованных возможностей стала "поддержка DI". Оказалось, что эта поддержка сосредоточена вокруг нового интерфейса с названием IDependencyResolver . Этот интерфейс и тот способ, при помощи которого он используется в фреймворке ASP.NET MVC, являются проблематичными. На концептуальном уровне предполагалось использовать IDependencyResolver в качестве Service Locator, и именно так фреймворк его и использует. На более конкретном уровне интерфейс обладает ограниченной полезностью, поскольку в нем отсутствует метод Release . Другими словами, мы не можем правильно управлять жизненным циклом диаграмм объектов посредством этого интерфейса. Для некоторых DI- контейнеров этот факт гарантирует утечку ресурсов. С учетом его текущего воплощения я считаю, что безопаснее и правильнее будет игнорировать IDependencyResolver . Ирония данной ситуации заключается в том, что истинный механизм внедрения зависимостей поддерживался ASP.NET MVC еще со времен первой его версии через интерфейс IControllerFactory С оздание пользовательской фабрики контроллеров ASP.NET MVC поставляется с DefaultControllerFactory , которая требует, чтобы классы Controller имели конструктор по умолчанию. Именно разумное поведение по умолчанию не вынуждает нас использовать механизм внедрения зависимостей, если мы этого не хотим. Тем не менее, конструкторы по умолчанию и механизм внедрения через конструктор являются взаимно исключающими, поэтому нам необходимо изменить это поведение посредством реализации пользовательско й фабрики контроллеров. Это не так уж сложно. Для этого необходимо реализовать интерфейс IControllerFactory public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior( RequestContext requestContext, string controllerName); void ReleaseController(IController controller); } 245 Метод CreateController предоставляет RequestContext , который содержит такую информацию, как HttpContext , тогда как controllerName указывает на то, какой контроллер запрашивается. Вы можете решить игнорировать RequestContext и использовать только controllerName для определения того, какой контроллер необходимо вернуть. Вне зависимости от того, что вы делаете, этот метод является местом, в котором вы получаете шанс подключить все необходимые зависимости и поставить их в контроллер прежде, чем вернуть экземпляр. Вы увидите пример в разделе "Пример: реализация Comm erceControllerFactory". Если вы создали какие-либо ресурсы, которые должны быть явно уничтожены, то вы можете сделать это при вызове метода ReleaseController П одсказка DefaultControllerFactory реализует IControllerFactory и имеет несколько виртуальных методов. Вместо того чтобы реализовывать IControllerFactory с самого начала, часто легче наследовать его от DefaultControllerFactory Несмотря на то, что реализация пользовательско й фабрики контроллеров является трудной частью, она не будет использоваться, пока мы не скажем о ней ASP.NET MVC. Ре гистрация пользовательской фабрики контролле ров Пользовательские фабрики контроллеров регистрируются как часть последовательности запуска приложения – обычно в файле Global.asax. Они регистрируются при помощи вызова ControllerBuilder.Current.SetControllerFactory . Ниже приведен фрагмент из шаблонного приложения Com merce: var controllerFactory = new CommerceControllerFactory(); ControllerBuilder.Current.SetControllerFactory(controllerFactory); Этот пример создает и определяет новый экземпляр пользовательской CommerceControllerFactory . ASP.NET MVC теперь будет использовать экземпляр controllerFactory в качестве фабрики контроллеров данного приложения. Если этот код кажется вам слегка знакомым, то это потому, что вы видели нечто похожее в разделе "Паттерны DI-контейнеров". Т огда я обещал показать вам, как реализовывать пользовательскую фабрику контроллеров, в главе "Построение объектов", и что? Это и есть глава "Паттерны DI-контейнеров". Пример: реализация CommerceControllerFactory Для шаблонного приложения Comm erce нужна пользовательская фабрика контроллеров, чтобы подсоединит ь необходимые зависимости к контроллерам. Несмотря на то, что полноценная диаграмма зависимостей для всех контроллеров значительно глубже, с точки зрения самих контроллеров, соединение всех ближайших зависимостей достигает трех элементов, что продемонстрировано на рисунке 7-6. 246 Рисунок 7-6: Диаграмма зависимостей для трех контроллеров шаблонного приложения Com merce. Конкретные реализации каждой из трех зависимостей имеют другие зависимости, но они здесь не показаны. BasketController и HomeController имеют одну общую зависимость CurrencyProvider AccountController унаследован неизмененным от шаблона ASP.NET MVC по умолчанию; поскольку он использует Bastard Injection, он не имеет неразрешенных зависимостей Несмотря на то, что вы могли бы реализовать IControllerFactory напрямую, проще наследовать его от DefaultControllerFactory и переопределить его метод GetControllerInstance . Это означает, что DefaultControllerFactory заботится о преобразовании имени контроллера в тип контроллера, и все, что вам приходится делать – это возвращать экземпляр ы необходимых типов. Листинг 7-3: Создание контроллеров 1. protected override IController GetControllerInstance( 2. RequestContext requestContext, Type controllerType) 3. { 4. string connectionString = 5. ConfigurationManager.ConnectionStrings 6. ["CommerceObjectContext"].ConnectionString; 7. var productRepository = 8. new SqlProductRepository(connectionString); 9. var basketRepository = 10. new SqlBasketRepository(connectionString); 11. var discountRepository = 12. new SqlDiscountRepository(connectionString); 13. var discountPolicy = 14. new RepositoryBasketDiscountPolicy( 15. discountRepository); 16. var basketService = 17. new BasketService(basketRepository, 18. discountPolicy); 19. var currencyProvider = new CachingCurrencyProvider( 20. new SqlCurrencyProvider(connectionString), 21. TimeSpan.FromHours(1)); 22. if (controllerType == typeof(BasketController)) 23. { 24. return new BasketController( 25. basketService, currencyProvider); 26. } 27. if (controllerType == typeof(HomeController)) 28. { 29. return new HomeController( 30. productRepository, currencyProvider); 31. } 32. return base.GetControllerInstance( 33. requestContext, controllerType); 34. } 247 Строка 1-2: Переопределяет Строка 4-21: Создает зависимости Строка 22-31: Возвращает подключенные контроллеры Строка 32-33: Использует базу для других контроллеров Этот метод переопределяет DefaultControllerFactory.GetControllerInstance для того, чтобы создать экземпляры необходимых типов контроллера. Если требуемый тип – это BasketController или HomeController , то вы явно соединяете их с необходимыми зависимостями и возвращаете их. Оба типа используют внедрение через конструктор, поэтому вы поставляете зависимости через их конструкторы. Для упрощения кода я решил соединить все зависимости до проверки controllerType Очевидно, это означает, что некоторые созданные зависимости не будут использоваться, поэтому это не слишком рациональная реализация. Вы можете выполнить рефакторинг листинга 7-3 в более подходящую (но слегка более сложную) форму. Для тех типов, которые не обрабатываются явно, вы по умолчанию обращаетесь к базовому поведению, которое заключает ся в создании необходимого контроллера при помощи его конструктора по умолчанию. Обратите внимание на то, что вы не обрабатываете явно AccountController , поэтому вместо этого вы позволяете базовому поведению справляться с AccountController AccountController является остатком шаблона ASP.NET MVC проекта и использует Bastard Injection, который дает ему конструктор по умолчанию. П римечание Я считаю Bastard Injection анти-паттерном, но я оставляю AccountController в таком состоянии, поскольку у меня есть множество других корректных примеров механизма внедрения зависимостей для демонстрации. Я сделал это, в конце концов, потому что это шаблонный код, но я бы никогда не оставил это в подобном состоянии в рабочем коде. После регистрации экземпляра CommerceControllerFactory в файле Global.asax он будет корректно создавать все необходимые контроллеры с необходимыми зависимостями. П одсказка Подумайте над тем, чтобы не писать пользовательскую фабрику контроллеров самостоятельно. Вместо этого используйте универсальную фабрику контроллеров, которая работает совместно с выбранным вами DI-контейнером. Для вдохновения посмотрите MVC Cont rib проект или используйте одну из доступных в нем многократно используемых реализаций. Некоторые DI-контейнеры также имеют "официальную" интеграцию с ASP.NET MVC. Прекрасно, что ASP.NET MVC был сконструирован таким образом, что уже подразумевал DI, поэтому нам всего лишь нужно знать и использовать единственное место расширяемости для того, чтобы разрешить DI для нашего приложения. В других фреймворках разрешение DI может быть гораздо более сложной задачей. W indows Communication Foundation (W CF), хотя и является расширяемым, выступает в качестве примера такого фреймворка. 248 7.3. Построение WCF приложений W CF – одна из самых расширяемых составляющих библиотеки стандартных классов (BCL). Несмотря на то, что начать писать W CF сервисы довольно легко, бесчисленные возможности расширяемости могут усложнить процесс поиска той возможности, которая нужна именно вам. Это еще один случай, когда в игру вступает механизм внедрения зависимостей. П римечание Согласно шутке, W CF – это акроним для Windows Com plication Foundation. В этом утверждении есть определенная доля правды. Вас легко можно было бы склонить к тому, что WCF не поддерживает внедрение через конструктор. Если вы будете реализовывать WCF сервис при помощи механизма внедрения через конструктор и без конструктора по умолчанию, то хостинг WCF сервиса в рабочей среде будет выдавать ServiceActivationException с сообщением, подобным следующему: Предоставленный тип сервиса не удалось загрузить в качестве сервиса, поскольку у него отсутствует конструктор по умолчанию (непараметризованный). Для устранения проблемы добавьте к типу конструктор по умолчанию или передайте экземпляр типа в хостинг. Это сообщение строго указывает на необходимость наличия конструктора по умолчанию. Кажется, что единственный выход – передать уже созданный экземпляр в WCF хостинг, но это приводит к нескольким проблемам: Как мы можем это сделать, если мы размещаем сервис на Int ernet Information |