Pull to refresh

Comments 27

UFO just landed and posted this here
Никто не ответит, потому что ответ — надо быть модным.
Тут в заголовке написано про CQRS, хотя на самом деле статья про медиатр (который неплох).
Не поясняются плюсы CQRS, не поясняются плюсы медиатра.

А самое главное, на HelloProduct это все не прочувствовать.
Статья, увы, не очень, если хотите более интересное и подробное описание и разбор CQRS, почитайте статьи Максима Аршинова habr.com/ru/users/marshinov
вот, например — habr.com/ru/post/353258
это проба пера и вообще моя первая статья) в следующей постараюсь получше охватить саму суть паттерна
данная статья это скорее туториал, как реализовать самый простой CQRS в вашем приложении в рамках ASP.NET Core 5 и некая точка для сбора фидбэка, чтобы понять, что людям хочется узнать. В вашем случае вы изолируете всё в подсистемы, но ваша подсистема может так же распухать как и контроллер и репозиторий, и точно также могут распухать и её зависимости, CQRS помогает избежать именно этого. в дальнейших статьях будут рассмотрены более сложные реализации, когда одно приложение выступает в качестве обработчика команд и кладет сырые данные в бд, а второе будет приложение будет возвращать вам подготовленные для вывода данные.

Медиатор в такой архитектуре служит для вынесения бизнес логики в application слой. А контроллеры реализуют исключительно presentation слой.

А можно подробней про остальные варианты — что имелось ввиду? И было бы здорово теперь все тоже самое, но когда бизнес логика посложнее обычного CRUD? Во внешний сервис пойти, в кэш, в очередь что-нибудь отправить. Или хотя бы получить из базы сущность, что-нибудь сгенерировать на основе того что в базе, что пришло с клиента и только потом сохранить. Ну и тяжелая артиллерия — какая-нибудь распределенная транзакция, охватывающая несколько query/command.

В чем вы видите сложность? Реализуете всю необходимую логику в обработчике медиатора. Через DI подтягиваете в него нужные сервисы, будь то репозитории, шины или внешние сервисы.

Наводящие вопросы, на которые мне приходилось как-то отвечать при работе в рамках подхода query/command (т.е. запрос — это выборка данных, без модификации, а команда — только модификация данных):
1. Куда деть дублирующийся бизнес-код, который используется в нескольких query/command? Например генерация и отправка уведомления. Генерация — здесь query или command? А отправка?
2. Если этот код выделить в IXYZService, то внутри таким сервисам может потребоваться добрать что-то еще из базы или даже сохранить (номер попытки отправки в БД? что-нибудь в лог?), т.е. вызов такой: command1 -> IXYZService -> query2/command2. И в какого монстра это все превращается?
3. Обращение к внешнему сервису — это query или command?
4. Как делать, если сначала надо получить сущность из базы через query, а потом сохранить через command? В query вызывать command или в command первым делом вызвать query? Может перенести это в контроллер? Тогда бизнес-логика размажется, да и контроллер начнет толстеть
5. Разрешать ли использовать внутри query вложенные query, а внутри command вложенные query/command. По канону нельзя
6. Как вообще ложатся методики описания бизнес-процессов (IDEF0 и BPMN) на одиночные query/command, которые нельзя вкладывать друг в друга?
7. А что у нас там с SOLID и другими принципами? Отдельные простые query/command еще могут отвечать этим принципам, а когда внутри все усложняется?

Изначально CQRS был создан для разделения хранилищ данных (основное для записи и несколько реплик для чтения). С тех пор как его стали использовать в качестве паттерна организации кода — проблемы полезли из всех щелей. И в оригинале он только и годится для CRUD. Но по другую сторону у нас те самые IXYZService сервисы, у которых есть риск превратиться в god-объекты. И query/command хорошее подспорье, чтобы это упростить и разделить.

PS Да и это я не имел ввиду EventSourcing — потому что это совсем отдельная тема.

Сугубо мое скромное мнение, в этой архитектуре CQRS применяется только ко внешним запросам, т.е. мы проектируем WEB API с оглядкой на CQRS. При этом медиатор служит не для CQRS запросов внутри приложения, а для вынесения логики в application слой, где уже в обработчиках запросов медиатора вы можете подтягивать через DI нужные сервисы, репозитории и т.д., выносить повторяющуюся логику в свои сервисы и вызвать их в нужных обработчиках.
Пусть автор поправит меня, если я не правильно понял.

выносить повторяющуюся логику в свои сервисы и вызвать их в нужных обработчиках

Вот это и есть «command1 -> IXYZService -> query2/command2». Как-то я работал на проекте, где IXYZService обращался к БД напрямую (через EF context). И все эти запросы были по сути подвисшими в нигде query/command.

Для запросов в БД можно сделать репозиторий и переиспользовать его в обработчиках.

Тем самым мы просто перенесем подвисший код из IXYZService в IXYZRepository, хотя по сути этот код тоже является query/command.

Конкретно в данном примере так и есть, но это только базовое использование этого подхода, где у вас query и command в одном приложении, дальше это расширяться может до бесконечности.


в полной версии у вас может быть три отдельных приложения, одно выполняет по сути crud сырых данных, приложение которое занимается чтением подготовленных данных для вывода, в том числе сложных объектов, которые при выводе требуют много джоинов и пр и приложение синхронизатор, который с помощью eventsourcing будет реагировать на изменения в сырых данных и обновлять подготовленные, таким образом мы сможем реплицировать только query часть, так как она и является наиболее загруженной в сервисах для вывода контента

в следующей статье я постараюсь поднять этот вопрос, как раз в deluxe версии там предполагается EventSourcing и так далее
Как при таком подходе будет выглядеть валидатор, который зависит от контекста? Что-то вроде добавлять продукты с определенными типами может только пользователем с определенным статусом. Или для продукта с таким-то типом alias может быть пустым.
При желании можно передать в конструктор пользователя и делать условные проверки.
Я не совсем понял кто «матчит» и вызывает валидатор для соотв. команды. интуиция подсказывает что медиатор, однако сначала сложилось впечатление что реализовывали мы интерфейсы из библиотеки флюент. Или IPipelineBehavior это из медиатора?

Судя по Startup это сделано следующим образом:

  1. Зарегистрировали конфиги БД, репозиторий, MediatR

  2. Добавили в DI валидаторы

    services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
  3. Добавили Behaviour для валидации

    services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
  4. Вызываем services.AddCustomMVC(Configuration) в котором происходит регистрация валидаторов

Ну и дальше уже DI все сам разрулит

Можно пожалуйста больше информации откуда вы взяли «Существует три вида паттерна CQRS: Regular, Progressive и Deluxe. » и где можно почитать детальнее?

Необычно, что валидатор навешивается на команды. Более аутентичный подход, как мне кажется, это валидировать входные данные, поступающие в контролеры (реквесты) тем самым не допуская конструирования невалидных команд. Получаем следующие плюсы:

  1. Не надо дополнительно конфигурировать FluentValidator - он на такой сценарий изначально рассчитан.

  2. При валидационных ошибках пользователь API получит ответ, в котором сообщения об ошибках соответствуют структуре реквеста, а не команды. При разветвлённой структуре это может быть важно.

  3. Не нужно пробрасыаать исключения при валидационных ошибках.

Из минусов я вижу то, что логика валидации конкретного реквеста связана с хендлером для команды очень неявным образом.

По своему опыту применения медатора, могу сказать, что он помогает в написании кода под конкретные юзкейсы, что особенно полезно, если одновременно работает несколько команд, и важно не поломать чужую логику в god-service. Но при этом, когда обсуждается паттерн "сервис-локатор", в качестве главного недостатка указывается, что у сервиса, имеющего зависимость от локатора, появляются невявные зависимости от других сервисов, при которые мы не можем узнать не глядя на реализацию; однако в случае с медатором мы имеем в точности то же самое, но я почему-то не встречал обсуждений такого недостатка этого паттерна.

Когда работаешь с медиатором, быстро вырабатывается привычка начинать изучение зависимостей с серединки - команды/запроса.

Как по мне тут так же может быть два слоя валидации, доменная и самих реквестов. История с исключениями поможет при доменной валидации, когда у вас есть сервис обрабатывающий доменную логику, например в доменной библиотеке и вам надо прокинуть ошибку наверх.

Уточните пожалуйста:
Command рассмотрели, а что с Query в данном случае?

Также описать dto для результата и также описать handler для медиатора? Если так, то в чем тогда принципиальная разница, кроме отличных друг от друга dto?

Пару замечаний:

  1. Я считаю, что критически важно отделить команды и запросы по разным проектам, чтобы они физически не могли никак пересечься;

  2. Команду/запрос, обработчик и валидацию стоит складывать в 1 файл. Тогда ты сразу его открываешь и видишь в одном файле всё что тебе нужно для реализации, исправления или рефакторинга.

Sign up to leave a comment.

Articles