Дизайн - Где должны регистрироваться объекты при использовании Windsor

У меня будут следующие компоненты в моем приложении

  • DataAccess
  • DataAccess.Test
  • Бизнес
  • Business.Test
  • Применение

Я надеялся использовать Castle Windsor в качестве IoC для склеивания слоев, но я немного сомневаюсь в дизайне склеивания.

Мой вопрос: кто должен отвечать за регистрацию объектов в Виндзоре? У меня есть пара идей:

  • Каждый слой может регистрировать свои собственные объекты. Чтобы протестировать BL, тестовый стенд мог регистрировать макеты для DAL.
  • Каждый слой может регистрировать объект своих зависимостей, например. бизнес-уровень регистрирует компоненты уровня доступа к данным. Чтобы протестировать BL, тест-стенд должен был бы выгрузить "реальный" объект DAL и зарегистрировать макет объектов.
  • Приложение (или тестовое приложение) регистрирует все объекты зависимостей.

Может кто-нибудь помочь мне с некоторыми идеями и плюсами/минусами с разными путями? Ссылки на примеры проектов, использующих Castle Windsor таким образом, были бы очень полезными.

Ответ 1

В общем, все компоненты в приложении должны быть составлены как можно позже, поскольку это обеспечивает максимальную модульность, и эти модули как можно более слабо связаны.

На практике это означает, что вы должны настроить контейнер в корне вашего приложения.

  • В настольном приложении это будет в основном методе (или очень близко к нему).
  • В приложении ASP.NET(включая MVC) это будет в Global.asax
  • В WCF это будет в ServiceHostFactory
  • и др.

Контейнер - это просто движок, который объединяет модули в рабочее приложение. В принципе, вы можете написать код вручную (это называется Bad Man DI), но гораздо проще использовать контейнер DI, такой как Windsor.

Такой кортеж композиции в идеале будет единственным фрагментом кода в корне приложения, что сделает приложение так называемым Humble Executable (термин из превосходного xUnit Test Patterns), который сам по себе не нуждается в модульном тестировании.

В ваших тестах не обязательно должен быть контейнер, так как ваши объекты и модули должны быть скомпонованы, и вы можете напрямую доставлять Test Doubles к ним из модульных тестов. Лучше всего, если вы можете сконструировать все свои модули как агрегированные по контейнеру.

Также в Windsor вы должны инкапсулировать логику регистрации компонентов в установщиках (типы, реализующие IWindsorInstaller). Подробнее см. документацию.

Ответ 2

В то время как ответ Mark отлично подходит для веб-сценариев, ключевой недостаток с его применением для всех архитектур (а именно rich-client - то есть: WPF, WinForms, iOS и т.д.) - это предположение, что все компоненты, необходимые для операции, должен быть создан сразу.

Для веб-серверов это имеет смысл, так как каждый запрос крайне недолговечен, и контроллер ASP.NET MVC создается базовым фреймворком (без кода пользователя) для каждого входящего запроса. Таким образом, контроллер и все его зависимости могут легко составляются каркасом DI, и для этого очень мало затрат на обслуживание. Обратите внимание, что веб-инфраструктура отвечает за управление временем жизни контроллера и для всех целей - временем жизни всех его зависимостей (которые структура DI будет создавать/вводить для вас при создании контроллера). Совершенно верно, что зависимости зависят от продолжительности запроса, а ваш код пользователя не требует управления временем жизни самих компонентов и подкомпонентов. Также обратите внимание, что веб-серверы не имеют состояния в разных запросах (кроме состояния сеанса, но это не имеет отношения к этому обсуждению) и что у вас никогда не бывает нескольких экземпляров контроллера/дочернего контроллера, которые должны одновременно работать для обслуживания одного запроса.

Однако в приложениях с богатым клиентом это не так. Если вы используете архитектуру MVC/MVVM (что вам нужно!), пользовательский сеанс долгоживущий, а контроллеры создают субконтроллеры /sibling, поскольку пользователь переходит через приложение (см. примечание о MVVM внизу). Аналогия с веб-миром заключается в том, что каждый пользовательский ввод (нажатие кнопки, операция) в приложении с богатым клиентом является эквивалентом запроса, получаемого веб-картой. Однако большая разница заключается в том, что вы хотите, чтобы контроллеры в приложении с богатым клиентом оставались в живых между операциями (очень возможно, что пользователь выполняет несколько операций на одном экране - который управляется конкретным контроллером), а также, что субконтроллеры получают созданный и уничтоженный, поскольку пользователь выполняет разные действия (подумайте о элементе управления вкладками, который лениво создает вкладку, если пользователь переходит к ней, или часть пользовательского интерфейса, которая требуется только для загрузки, если пользователь выполняет определенные действия на экране).

Обе эти характеристики означают, что это код пользователя, который должен управлять временем жизни контроллеров/субконтроллеров и что зависимости контроллеров НЕ должны быть созданы заранее (то есть: субконтроллеры, модели просмотра, другие компоненты презентации и т.д.). Если вы используете структуру DI для выполнения этих обязанностей, вы получите не только намного больше кода, где он не принадлежит (см. Конструктор over-injection anti-pattern), но вам также необходимо пройти по контейнеру зависимостей на большинстве слоев презентации, чтобы ваши компоненты могли использовать его для создания своих подкомпонентов, когда это необходимо.

Почему плохо, что мой пользовательский код имеет доступ к контейнеру DI?

1) Контейнер зависимостей содержит ссылки на множество компонентов в вашем приложении. Передача этого плохого мальчика каждому компоненту, который должен создать/управлять подкомпонентом anoter, эквивалентен использованию глобальных переменных в вашей архитектуре. Хуже того, любой подкомпонент может также регистрировать новые компоненты в контейнере, поэтому достаточно скоро он станет глобальным хранилищем. Разработчики будут бросать объекты в контейнер только для того, чтобы передавать данные между компонентами (либо между диспетчерами сестер, либо между иерархиями глубоких контроллеров, то есть: контроллеру предка необходимо захватить данные от контроллера grandparent). Обратите внимание, что в веб-мире, где контейнер не передается в пользовательский код, это никогда не проблема.

2) Другая проблема с контейнерами зависимостей по сравнению с установщиками локаций/фабрик/объектов прямого объекта заключается в том, что разрешение из контейнера делает его полностью двусмысленным, являетесь ли вы СОЗДАНИЕМ компонента или просто ОТКЛЮЧАЙТЕ существующий. Вместо этого он остается до централизованной конфигурации (то есть: bootstrapper/Roose of Composition), чтобы выяснить, что такое время жизни компонента. В некоторых случаях это нормально (т.е. Веб-контроллеры, где это не код пользователя, которому необходимо управлять временем жизни компонента, но сама среда обработки запросов времени исполнения). Это чрезвычайно проблематично, однако, когда дизайн ваших компонентов должен указывать, должна ли им быть ответственна за управление компонентом и каким он должен быть (Пример: телефонное приложение вытаскивает лист, который запрашивает у пользователя некоторую информацию. контроллер, создающий субконтроллер, который управляет наложением листа. Как только пользователь вводит какую-либо информацию, лист сбрасывается, и управление возвращается первоначальному контроллеру, который все еще сохраняет состояние от того, что пользователь делал ранее). Если DI используется для разрешения подконтролятеля листа, он неоднозначен, каково его время жизни или кто должен нести ответственность за его управление (инициирующий контроллер). Сравните это с явной ответственностью, продиктованной использованием других механизмов.

Сценарий A:

// not sure whether I'm responsible for creating the thing or not
DependencyContainer.GimmeA<Thing>()

Сценарий B:

// responsibility is clear that this component is responsible for creation

Factory.CreateMeA<Thing>()
// or simply
new Thing()

Сценарий C:

// responsibility is clear that this component is not responsible for creation, but rather only consumption

ServiceLocator.GetMeTheExisting<Thing>()
// or simply
ServiceLocator.Thing

Как вы можете видеть, DI не ясно, кто отвечает за управление временем подкомпонента.

Примечание:Технически говоря, многие рамки DI имеют некоторый способ создания компоненты лениво (см. Как не делать инъекции зависимостей - статический или однотонный контейнер), что намного лучше, чем передача контейнер вокруг, но вы по-прежнему оплачиваете стоимость мутации вашего кода, чтобы обойти все функции создания везде, вам не хватает первого уровня поддержка передачи в действительных параметрах конструктора во время создания, и в конце дня вы все еще используете механизм косвенности без необходимости в тех местах, где единственное преимущество заключается в достижении проверяемости, которые могут быть достигнуты лучшими, более простыми способами (см. ниже).

Что все это значит?

Это означает, что DI подходит для определенных сценариев и неприемлемо для других. В приложениях с богатым клиентом он, как оказалось, несут много недостатков DI с очень небольшим количеством факторов. Чем дальше ваше приложение масштабируется по сложности, тем больше затраты на обслуживание будут расти. Он также несет в себе серьезный потенциал для злоупотребления, который зависит от того, насколько плотно взаимодействие вашей команды и процессов проверки кода, может быть где угодно от не-выпуска до серьезной стоимости долга. Существует миф о том, что Сервис Локаторы или Фабрики или старые добрые Instantiation являются как-то плохие и устаревшие механизмы просто потому, что они могут быть не оптимальным механизмом в мире веб-приложений, где, возможно, много людей играют дюйма Мы не должны чрезмерно- обобщать эти знания для всех сценариев и рассматривать все как гвозди только потому, что мы научились использовать конкретный молот.

Моя рекомендация ДЛЯ RICH-CLIENT APPS - использовать минимальный механизм, отвечающий требованиям для каждого компонента. 80% времени это должно быть прямое создание. Локаторы сервисов могут использоваться для размещения ваших основных компонентов бизнес-уровня (например, приложений, которые, как правило, имеют одноэлементный характер), и, конечно же, их производят заводы и даже шаблон Singleton. Нельзя сказать, что вы не можете использовать инфраструктуру DI, скрытую за вашим локатором сервисов, для создания зависимостей бизнес-уровня и всего, от чего они зависят, за один раз - если это в конечном итоге облегчит вашу жизнь в этом слое, и что слой не показывает ленивую загрузку, в которой слои презентаций с богатым клиентом в подавляющем большинстве делают. Просто убедитесь, что вы защищаете свой код пользователя от доступа к этому контейнеру, чтобы вы могли предотвратить создание беспорядка, который может создать контейнер DI.

Как насчет тестируемости?

Испытуемость может быть абсолютно достигнута без рамки DI. Я рекомендую использовать инфраструктуру перехвата, такую ​​как UnitBox (бесплатно) или TypeMock(Дорогой). Эти рамки предоставляют вам инструменты, необходимые для решения проблемы (как вы издеваетесь над созданием экземпляров и статических вызовов на С#) и не требуете, чтобы вы изменили всю свою архитектуру, чтобы обойти их (к сожалению, там, где наблюдается тенденция вошли в мир .NET/Java). Разумно найти решение проблемы и использовать механизмы и шаблоны естественного языка, оптимальные для базового компонента, а затем попытаться установить каждую квадратную привязку в круглое отверстие DI. Как только вы начнете использовать эти более простые, более конкретные механизмы, вы заметите, что в вашей кодовой базе очень мало потребности в DI.

ПРИМЕЧАНИЕ. Для архитектуры MVVM

В основных моделях MVVM-моделей эффективно используются ответственность диспетчеров, поэтому для всех целей "контроллер", приведенный выше, для применения к "модели представлений". Основные работы MVVM отлично подходит для небольших приложений, но по мере роста сложности приложения вы можете захотеть для использования подхода MVCVM. Модели просмотра становятся в основном немыми DTO облегчить привязку данных к представлению, в то время как взаимодействие с бизнес-уровня и между группами представлений-моделей, представляющими экраны/субэкраны инкапсулируются в явные контроллера/субконтроллера. В любой архитектуре ответственность диспетчеров существует и демонстрирует то же самое характеристик, рассмотренных выше.