Здравствуйте!
Это еще один пост из раздела «Дизайн», и на этот раз я привожу содержание в другом формате. Я не собираюсь подробно объяснять архитектурный паттерн CQRS, но прояснить некоторые моменты, связанные с ним, которые часто путают — и очень широко! — сообществом .NET.
Я начну с двух провокаций:
- CQRS — это не про MediatR;
- CQRS без комплексного домена — это не CQRS!
Потеряли? Я скажу прямо. Поехали!
Цель шаблона
Когда Грег Янг предложил этот паттерн в 2010 году, проблема, которую он хотел решить, заключалась в невозможности согласовать богатую модель домена, а точнее DDD Aggregate, с архитектурным паттерном, управляемым данными, который поддерживает только 4 глагола (Create, Read, Update и Delete, знаменитый CRUD).
Когда мы используем DDD, наша главная задача — иметь понятный жаргон, то, что Эрик Эванс назовет «вездесущим (или вездесущим) языком». То есть, если в данной доменной модели есть глагол (который преобразуется в метод) под названием, например, «Отменить», то в архитектуре на основе CRUD просто не будет соответствующего метода для этого глагола (обратите внимание, что в случае покупки, например, «Отменить» — это не то же самое, что «Удалить/Исключить»).
Помимо этого, есть две проблемы, касающиеся пользовательского интерфейса: 1) интерфейсы типа CRUD интересуются исключительно состоянием DTO, который его питает, в то время как в DDD нас больше интересует поведение доменной модели. Это требует стиля разработки интерфейса, известного как «интерфейс, основанный на задаче». То есть, выполняемое действие важнее, чем состояние модели и; 2) не всегда этот интерфейс требует все данные, содержащиеся в доменной модели, что приводит к необходимости создания отображений между вашей доменной моделью и DTO, отправляемым пользователю, что, помимо того, что это дополнительная работа, связывает ваш домен с вашим прикладным уровнем (вашим контроллером, сценарием использования, или каким бы ни было его представление).
Учитывая это, Грег Янг решил подумать о том, как создать архитектурный паттерн, который бы решал эти проблемы.
Происхождение узора
Грег Янг основывался на идее, предложенной Бертраном Мейером, которая, по сути, гласила следующее:
«Операция, которая изменяет состояние объекта, не должна возвращать значение. А операция, возвращающая значение, не должна изменять состояние объекта».
Это предложение стало известно как CQS, что в переводе с португальского означает «Разделение между запросами и командами».
Однако в отличие от идеи CQS, которая в конечном итоге должна быть нарушена (как в методе pop массива, который изменяет его состояние, удаляя элемент, но также возвращает его), CQRS предназначен для того, чтобы оставаться чистым. То есть, команда не должна возвращать значение (хотя она может вернуть результат), а запрос не должен выполнять команду.
Однако наиболее важным аспектом является не просто разделение между командами и запросами, а, скорее, разделение между моделью домена и моделью представления (DTO). И здесь я начинаю отвечать на вышеупомянутые провокации.
CQRS не имеет никакого отношения к MediatR
Многие люди считают, повторяя неверную информацию или из сомнительных источников, что достаточно сделать реализацию с помощью MediatR, создать CommandHandlers и QueryHandlers и, вуаля, у нас есть CQRS.
Ну. Это мнение совершенно неверно!
Помните, что было сказано выше: более важным, чем отделение команд от запросов, является отделение доменной модели от модели представления. Поэтому, если вы сделаете только реализацию с MediatR, но продолжите использовать DTO, которые представляют состояние вашей доменной модели (которая, как правило, отражает вашу модель персистентности), вы только добавите ненужный код и, потенциально, создадите избыточность, например, повторите в вашем персистентном уровне структуру DTO, используемую прикладным уровнем. Вы можете сказать, что вы используете паттерн медиатора, но не то, что вы реализуете CQRS!
Примечание: Я не претендую на роль надежного источника. Я просто воспроизвожу авторское содержание шаблона, и я подвержен неправильному толкованию. Поэтому любая критика, которая направит меня в русло этого содержания, будет очень приветствоваться!
Кроме того, есть и вторая деталь: MediatR даже не нужен для внедрения CQRS. Не обязательно, чтобы ваш уровень приложения (например, контроллер в вашем Web API) имел знания о CommandHandHandler или QueryHandler — это не означает, что использование MediatR будет накладным, поскольку оно требует создания нового экземпляра медиатора при каждой активации.
Примечание: Конечно, если вы понимаете, что этот компромисс между производительностью и развязкой имеет положительную связь, нет причин не использовать его, у меня даже есть учебник по MediatR, опубликованный в блоге. Кстати, сделайте тест! Создайте Web API с MediatR и без него и проведите тест производительности. Это веселое упражнение!
Тогда вы можете спросить меня: но в этом случае я буду внедрять много обработчиков в свой контроллер?
И ответ: определенно нет!
Лично я не рекомендую использовать один и тот же контроллер для выполнения более чем одной операции в этом случае, предполагая, что ваш контроллер эквивалентен вашему Use Case. Альтернативой является организация контроллеров не на основе модели домена, на которую они влияют (например, OrderController), а на основе действий пользователя — помните идею интерфейса, основанного на задачах? Именно здесь, на уровне приложений, это имеет смысл. Другими словами, следуя приведенному выше примеру, у нас был бы PlaceOrderController, который бы инжектировал PlaceOrderCommandHandler (или экземпляр IMediatR) и выполнил остальную работу.
CQRS без сложного домена не является CQRS
Я думаю, что этот момент уже ясен. Но я хочу добавить несколько комментариев.
Существует несколько возможных стратегий для отделения модели домена от модели представления, поэтому я приведу их для ознакомления.
События и прогнозы
Это первый способ, который рекомендует Грег Янг. Когда команда выполняется, и состояние доменной модели изменяется, возникает событие домена, и на его основе обновляется модель представления. В этом сценарии будет две области персистентности: одна для модели домена, другая для модели представления.
Обратите внимание, что в этом случае согласованность обязательно будет временной, так как между запуском события, его захватом, обработкой и обновлением модели представления пройдет некоторое время.
Примечание: очень важным вопросом в этой ситуации является гарантия порядка в обработке событий. Если события обрабатываются не по порядку, состояние как вашей доменной модели, так и вашей модели представления будет нарушено. На это следует обратить внимание, выбирая эту обновленную модель.
События как постоянство
Это второй способ, рекомендованный Грегом Янгом. Точно так же, как и выше, событие домена запускается после выполнения команды, но вместо обновления состояния модели домена, само событие будет храниться, а состояние модели будет отражением обработки этих событий в порядке возникновения — и вот мы имеем еще один паттерн, называемый Event Sourcing.
Заключение
Как мы могли видеть выше, CQRS — это гораздо больше, чем просто разделение некоторых частей кода и включение MediatR в качестве своего рода локатора услуг. Если у вас нет сложного домена, который требует такого разграничения между моделями домена и моделями представления, то будет интересно отказаться от этого подхода, чтобы сохранить простоту кода.
В противном случае стоит рассмотреть возможность использования CQRS вместе с Event Sourcing, поскольку эти модели дополняют друг друга.
Сообщите мне о ваших показателях.
Есть вопросы, критика или предложения? Оставьте комментарий или найдите меня в социальных сетях.
Увидимся в следующий раз!