Comments 27
Тут в заголовке написано про CQRS, хотя на самом деле статья про медиатр (который неплох).
Не поясняются плюсы CQRS, не поясняются плюсы медиатра.
А самое главное, на HelloProduct это все не прочувствовать.
вот, например — habr.com/ru/post/353258
Медиатор в такой архитектуре служит для вынесения бизнес логики в application слой. А контроллеры реализуют исключительно presentation слой.
А можно подробней про остальные варианты — что имелось ввиду? И было бы здорово теперь все тоже самое, но когда бизнес логика посложнее обычного CRUD? Во внешний сервис пойти, в кэш, в очередь что-нибудь отправить. Или хотя бы получить из базы сущность, что-нибудь сгенерировать на основе того что в базе, что пришло с клиента и только потом сохранить. Ну и тяжелая артиллерия — какая-нибудь распределенная транзакция, охватывающая несколько query/command.
В чем вы видите сложность? Реализуете всю необходимую логику в обработчике медиатора. Через DI подтягиваете в него нужные сервисы, будь то репозитории, шины или внешние сервисы.
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.
Конкретно в данном примере так и есть, но это только базовое использование этого подхода, где у вас query и command в одном приложении, дальше это расширяться может до бесконечности.
в полной версии у вас может быть три отдельных приложения, одно выполняет по сути crud сырых данных, приложение которое занимается чтением подготовленных данных для вывода, в том числе сложных объектов, которые при выводе требуют много джоинов и пр и приложение синхронизатор, который с помощью eventsourcing будет реагировать на изменения в сырых данных и обновлять подготовленные, таким образом мы сможем реплицировать только query часть, так как она и является наиболее загруженной в сервисах для вывода контента
Судя по Startup это сделано следующим образом:
Зарегистрировали конфиги БД, репозиторий, MediatR
Добавили в DI валидаторы
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
Добавили Behaviour для валидации
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
Вызываем
services.AddCustomMVC(Configuration)
в котором происходит регистрация валидаторов
Ну и дальше уже DI все сам разрулит
У Дино Эспозито есть презентация на эту тему https://youtu.be/CrBi33fpFh4
Необычно, что валидатор навешивается на команды. Более аутентичный подход, как мне кажется, это валидировать входные данные, поступающие в контролеры (реквесты) тем самым не допуская конструирования невалидных команд. Получаем следующие плюсы:
Не надо дополнительно конфигурировать FluentValidator - он на такой сценарий изначально рассчитан.
При валидационных ошибках пользователь API получит ответ, в котором сообщения об ошибках соответствуют структуре реквеста, а не команды. При разветвлённой структуре это может быть важно.
Не нужно пробрасыаать исключения при валидационных ошибках.
Из минусов я вижу то, что логика валидации конкретного реквеста связана с хендлером для команды очень неявным образом.
По своему опыту применения медатора, могу сказать, что он помогает в написании кода под конкретные юзкейсы, что особенно полезно, если одновременно работает несколько команд, и важно не поломать чужую логику в god-service. Но при этом, когда обсуждается паттерн "сервис-локатор", в качестве главного недостатка указывается, что у сервиса, имеющего зависимость от локатора, появляются невявные зависимости от других сервисов, при которые мы не можем узнать не глядя на реализацию; однако в случае с медатором мы имеем в точности то же самое, но я почему-то не встречал обсуждений такого недостатка этого паттерна.
Когда работаешь с медиатором, быстро вырабатывается привычка начинать изучение зависимостей с серединки - команды/запроса.
Как по мне тут так же может быть два слоя валидации, доменная и самих реквестов. История с исключениями поможет при доменной валидации, когда у вас есть сервис обрабатывающий доменную логику, например в доменной библиотеке и вам надо прокинуть ошибку наверх.
Command рассмотрели, а что с Query в данном случае?
Также описать dto для результата и также описать handler для медиатора? Если так, то в чем тогда принципиальная разница, кроме отличных друг от друга dto?
Пару замечаний:
Я считаю, что критически важно отделить команды и запросы по разным проектам, чтобы они физически не могли никак пересечься;
Команду/запрос, обработчик и валидацию стоит складывать в 1 файл. Тогда ты сразу его открываешь и видишь в одном файле всё что тебе нужно для реализации, исправления или рефакторинга.
Паттерн CQRS: теория и практика в рамках ASP.Net Core 5