Дизайн - Где должны быть зарегистрированы объекты при использовании Windsor
У меня будут следующие компоненты в моем приложении
- Доступ к данным
- DataAccess.Test
- Бизнес
- Business.Test
- заявка
Я надеялся использовать Castle Windsor в качестве IoC для склеивания слоев, но я немного не уверен в дизайне склеивания.
У меня вопрос, кто должен отвечать за регистрацию объектов в Виндзоре? У меня есть пара идей;
- Каждый слой может зарегистрировать свои собственные объекты. Для проверки BL тестовый стенд мог зарегистрировать фиктивные классы для DAL.
- Каждый уровень может регистрировать объект своих зависимостей, например, бизнес-уровень регистрирует компоненты уровня доступа к данным. Чтобы проверить BL, испытательный стенд должен был бы выгрузить "настоящий" объект DAL и зарегистрировать фиктивные объекты.
- Приложение (или тестовое приложение) регистрирует все объекты зависимостей.
Может ли кто-нибудь помочь мне с некоторыми идеями и плюсами / минусами с разными путями? Ссылки на примеры проектов, использующих Castle Windsor таким образом, были бы очень полезны.
2 ответа
В общем, все компоненты в приложении должны быть составлены как можно позже, потому что это обеспечивает максимальную модульность, а модули должны быть как можно слабее связаны.
На практике это означает, что вы должны настроить контейнер в корне вашего приложения.
- В настольном приложении это будет в методе Main (или очень близко к нему)
- В приложении ASP.NET (включая MVC) это будет в Global.asax
- В WCF это было бы в ServiceHostFactory
- и т.п.
Контейнер - это просто механизм, который объединяет модули в работающее приложение. В принципе, вы можете написать код вручную (это называется DI Бедного Человека), но гораздо проще использовать DI-контейнер, такой как Windsor.
Такой корень композиции в идеале должен быть единственным фрагментом кода в корне приложения, что делает приложение так называемым Humble Executable (термин из превосходных тестовых шаблонов xUnit), которое само по себе не нуждается в модульном тестировании.
Ваши тесты вообще не должны нуждаться в контейнере, поскольку ваши объекты и модули должны быть компонуемыми, и вы можете напрямую предоставить им Test Doubles из модульных тестов. Лучше всего, если вы сможете спроектировать все свои модули так, чтобы они не зависели от контейнера.
Также конкретно в Виндзоре вы должны инкапсулировать логику регистрации компонентов в установщиках (типы, реализующие IWindsorInstaller
) Смотрите документацию для более подробной информации
Хотя ответ Марка хорош для веб-сценариев, ключевым недостатком его применения для всех архитектур (а именно, rich-client - т. Е. WPF, WinForms, iOS и т. Д.) Является предположение, что все компоненты, необходимые для операции, могут / должны быть созданы однажды.
Для веб-серверов это имеет смысл, поскольку каждый запрос является чрезвычайно коротким, а контроллер ASP.NET MVC создается базовой платформой (без пользовательского кода) для каждого входящего запроса. Таким образом, контроллер и все его зависимости могут быть легко составлены структурой DI, и затраты на обслуживание очень малы. Обратите внимание, что веб-инфраструктура отвечает за управление временем жизни контроллера и для всех целей - временем жизни всех его зависимостей (которые среда DI создаст / внедрит для вас при создании контроллера). Совершенно нормально, что зависимости живут в течение всего запроса, и ваш пользовательский код не должен управлять временем жизни компонентов и самих подкомпонентов. Также обратите внимание, что веб-серверы не имеют состояния для разных запросов (за исключением состояния сеанса, но это не имеет значения для этого обсуждения) и что у вас никогда не будет нескольких экземпляров контроллера / дочернего контроллера, которым необходимо одновременно работать для обслуживания одного запроса.
Однако в приложениях для богатых клиентов это не совсем так. Если используется архитектура MVC/MVVM (что и должно быть!), Сеанс пользователя является долгоживущим, и контроллеры создают субконтроллеры / контроллеры одного уровня, когда пользователь перемещается по приложению (см. Примечание о MVVM внизу). Аналогия с веб-миром состоит в том, что каждый пользовательский ввод (нажатие кнопки, выполненная операция) в приложении с расширенным клиентом является эквивалентом запроса, получаемого веб-структурой. Однако большая разница заключается в том, что вы хотите, чтобы контроллеры в приложении с расширенными возможностями клиента оставались в живых между операциями (очень возможно, что пользователь выполняет несколько операций на одном экране - который управляется конкретным контроллером), а также чтобы субконтроллеры получали созданный и уничтоженный, когда пользователь выполняет различные действия (подумайте о элементе управления вкладкой, который лениво создает вкладку, если пользователь переходит на нее, или об элементе пользовательского интерфейса, который необходимо загрузить, только если пользователь выполняет определенные действия на экране).
Обе эти характеристики означают, что именно пользовательский код должен управлять временем жизни контроллеров / субконтроллеров, и что зависимости контроллеров НЕ ДОЛЖНЫ создаваться заранее (т.е. субконтроллеры, модели представления, другие компоненты представления и т. Д.).). Если вы будете использовать DI-фреймворк для выполнения этих обязанностей, вы получите не только намного больше кода, к которому он не относится (см. Конструктор анти-шаблонных вложений), но вам также потребуется передавать контейнер зависимостей на протяжении всего процесса. большая часть вашего уровня представления, чтобы ваши компоненты могли использовать его для создания своих подкомпонентов при необходимости.
Почему плохо, что мой код пользователя имеет доступ к контейнеру DI?
1) Контейнер зависимостей содержит ссылки на множество компонентов в вашем приложении. Передача этого плохого парня каждому компоненту, который нуждается в создании / управлении подкомпонентом anoter, эквивалентна использованию глобальных переменных в вашей архитектуре. Ухудшение любого подкомпонента может также зарегистрировать новые компоненты в контейнере, так что достаточно скоро он станет глобальным хранилищем. Разработчики будут бросать объекты в контейнер просто для передачи данных между компонентами (либо между контроллерами-соседями, либо между глубокими иерархиями контроллеров, т. Е. Контроллер-предок должен получать данные от контроллера-прародителя). Обратите внимание, что в веб-мире, где контейнер не передается пользовательскому коду, это никогда не является проблемой.
2) Другая проблема, связанная с контейнерами зависимостей по сравнению с локаторами / фабриками служб / непосредственным созданием объектов, заключается в том, что разрешение из контейнера делает совершенно двусмысленным, создаете ли вы компонент или просто повторно используете существующий. Вместо этого это оставлено до централизованной конфигурации (то есть: загрузчик / Корень Композиции), чтобы выяснить, каково время жизни компонента. В некоторых случаях это нормально (т.е. веб-контроллеры, где не пользовательский код должен управлять временем жизни компонента, а сама среда обработки запросов во время выполнения). Это крайне проблематично, однако, когда дизайн ваших компонентов должен УКАЗАТЬ, является ли их обязанностью управлять компонентом, и каков его срок службы (пример: приложение для телефона выскакивает лист, который запрашивает у пользователя некоторую информацию. Это достигается контроллер создает субконтроллер, который управляет оверлейным листом. Как только пользователь вводит некоторую информацию, лист уходит в отставку, и управление возвращается исходному контроллеру, который все еще поддерживает состояние от того, что пользователь делал ранее). Если DI используется для разрешения субконтроллера листа, то неоднозначно, каким должен быть срок его службы или кто должен отвечать за его управление (инициирующий контроллер). Сравните это с явной ответственностью, продиктованной использованием других механизмов.
Сценарий А:
// not sure whether I'm responsible for creating the thing or not
DependencyContainer.GimmeA<Thing>()
Сценарий Б:
// 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 и очень мало минусов. Чем сложнее масштабируется ваше приложение, тем больше будут расти расходы на обслуживание. Это также несет в себе серьезный потенциал для неправильного использования, которое в зависимости от того, насколько напряженными являются процессы взаимодействия вашей команды и проверки кода, может быть где угодно, от отсутствия проблем до серьезных затрат на техническую задолженность. Существует миф о том, что сервис-локаторы или фабрики, или старые добрые инстанциации являются каким-то плохим и устаревшим механизмом просто потому, что они не могут быть оптимальным механизмом в мире веб-приложений, где, возможно, играет много людей. Мы не должны переусердствовать. обобщите эти уроки для всех сценариев и рассматривайте все как гвозди только потому, что мы научились владеть конкретным молотком.
Моя рекомендация для приложений RICH-CLIENT - использовать минимальный механизм, который отвечает требованиям для каждого компонента под рукой. 80% времени это должно быть прямое мгновение. Локаторы сервисов могут использоваться для размещения основных компонентов бизнес-уровня (т. Е. Сервисов приложений, которые обычно имеют одноэлементный характер), и, разумеется, фабрики и даже шаблон Singleton также имеют свое место. Ничто не говорит о том, что вы не можете использовать инфраструктуру DI, спрятанную за локатором сервисов, для создания зависимостей вашего бизнес-уровня и всего, от чего они зависят, за один раз - если это в конечном итоге делает вашу жизнь проще в этом слое, а этот слой не ' t демонстрирует ленивую загрузку, которую в подавляющем большинстве делают слои представления богатых клиентов. Просто убедитесь, что ваш пользовательский код защищен от доступа к этому контейнеру, чтобы вы могли избежать беспорядка, который может создавать контейнер DI.
Как насчет тестируемости?
Тестируемость может быть абсолютно достигнута без структуры DI. Я рекомендую использовать среду перехвата, такую как UnitBox (бесплатно) или TypeMock (дорогой). Эти платформы предоставляют вам инструменты, необходимые для решения проблемы (как вы макетируете инстанцирование и статические вызовы в C#), и не требуют, чтобы вы меняли всю архитектуру, чтобы обойти их (что, к сожалению, является тенденцией ушел в мир.NET/Java). Разумнее найти решение проблемы и использовать механизмы и шаблоны естественного языка, оптимальные для базового компонента, а затем попытаться вписать каждый квадратный колышек в круглое отверстие DI. Как только вы начнете использовать эти более простые, более специфические механизмы, вы заметите, что DI в вашей кодовой базе практически не требуется, если вообще существует.
ПРИМЕЧАНИЕ. Для архитектур MVVM
В базовых архитектурах MVVM модели представления эффективно принимают на себя ответственность контроллеров, поэтому для всех целей рассмотрите приведенную выше формулировку "контроллер" для применения к "модели представления". Базовый MVVM отлично работает для небольших приложений, но по мере роста сложности приложения вы можете захотеть использовать подход MVCVM. Модели представлений становятся в основном тупыми DTO для облегчения привязки данных к представлению, в то время как взаимодействие с бизнес-уровнем и между группами моделей представлений, представляющих экраны / подэкраны, инкапсулируется в явные компоненты контроллера / субконтроллера. В любой архитектуре ответственность контроллеров существует и обладает теми же характеристиками, которые обсуждались выше.