Command bus и CQRS

Adelf

Administrator
Команда форума
Т.е. мне из CQRS почти не нужен комманбас, нужно разделение на команды и квери и разделение на рид и райт модели.
Коммандбас не является частью CQRS. Это просто паттерн для организации... не знаю как назвать, например: слоя команд. Не хочется - не юзай. Никто не заставляет.
Вот тут пример упрощенного слоя команд. Твои нелюбимые сервис классы, но пофиг - https://github.com/adelf/freelance-example/blob/master/app/Services/ClientsService.php. Именно так как ты говоришь: когда мало параметров можно просто одним параметром, без всяких дто. Но по честному, эта штука из-за того что параметры могут менять - не очень стабильная получается. Дто даёт ещё некую стабильность интерфейса, хоть и мнимую)
Идея коммандбаса мне нравится: оно даёт гибкость.
 

Yoskaldyr

"Спамер"
Партнер клуба
Коммандбас не является частью CQRS
в теории нет, но де факто - да. Все примеры, все стандартные реализации - только через комманд бас. И если смотреть эти же примеры и реализации, то может создаться впечатление что CQRS это о комманд басе, а не о разделениии на команды и квери и не о разделении о рид и райт моделей. И самое печальное что большинство разработчиков так неправильно и понимает и чуть ли ставит равенство между CQRS и комманд басом, вернее что комманд бас - это неотъемлемая часть CQRS и без комманд баса CQRS это не CQRS.
 

fixxxer

К.О.
Партнер клуба
Ты можешь вместо коммандбаса использовать некое подобие маршрутизатора, если тебе так удобнее. От этого ничего принципиально не изменится.
 

AmdY

Пью пиво
Команда форума
Коммандбас не является частью CQRS. Это просто паттерн для организации... не знаю как назвать, например: слоя команд. Не хочется - не юзай. Никто не заставляет.
Вот тут пример упрощенного слоя команд. Твои нелюбимые сервис классы, но пофиг - https://github.com/adelf/freelance-example/blob/master/app/Services/ClientsService.php. Именно так как ты говоришь: когда мало параметров можно просто одним параметром, без всяких дто. Но по честному, эта штука из-за того что параметры могут менять - не очень стабильная получается. Дто даёт ещё некую стабильность интерфейса, хоть и мнимую)
Идея коммандбаса мне нравится: оно даёт гибкость.
А зачем тебе там сервис. Почему бы сразу в контроллере не бросать ClientRegistration, избавился бы и от ClientsService::register и от Client::register. Получается что в двух местах навёрнута лишняя логика?
 

Adelf

Administrator
Команда форума
Почему бы сразу в контроллере не бросать ClientRegistration
ты имеешь ввиду команду в сервис бас? Тогда ничего особо сэкономить не получится.
А так, это мелкий демо проектик, где я просто хотел показать как выделить слой приложения от контроллеров. и как выделить логику домена от слоя приложения. разумеется, для регистрации клиента в этом смысла особого нет.
 

fixxxer

К.О.
Партнер клуба
Коммандбас не является частью CQRS. Это просто паттерн для организации... не знаю как назвать, например: слоя команд. Не хочется - не юзай. Никто не заставляет.
Вот тут пример упрощенного слоя команд. Твои нелюбимые сервис классы, но пофиг - https://github.com/adelf/freelance-example/blob/master/app/Services/ClientsService.php. Именно так как ты говоришь: когда мало параметров можно просто одним параметром, без всяких дто. Но по честному, эта штука из-за того что параметры могут менять - не очень стабильная получается. Дто даёт ещё некую стабильность интерфейса, хоть и мнимую)
Идея коммандбаса мне нравится: оно даёт гибкость.
О, а вот как ты определяешь, к чему принадлежат Read Models?
Read Models зачастую не относятся ни к какому Bounded Context (да и по идее вообще к ним, строго говоря, не относятся), но что-то же с ними надо делать. В таком небольшом примере все в кучку нормально, а если проект большой?
Мне не нравится разделение по типам классов, я предпочитаю делить по слоям и контекстам.
 

Yoskaldyr

"Спамер"
Партнер клуба
А вообще реально бы посмотреть какой-то большой проект по CQRS с красивым разделением на рид и райт модели. github выдает только поделки-примеры которые оооочень далеки от реальных проектов.
 

Yoskaldyr

"Спамер"
Партнер клуба
подправил заголовок темы чтобы потом легче искать было
 

fixxxer

К.О.
Партнер клуба
Ты очень много вопросов задаешь по несущественным мелочам. :)

Тут tutorial based development не прокатит. Просто бери и начинай делать, руководствуясь изученной литературой и здравым смыслом. Где сомневаешься, руководствуйся принципами SOLID и здравым смыслом. Ну накосячишь по первости - ну так а кто рефакторинг отменял?
 

Yoskaldyr

"Спамер"
Партнер клуба
Просто не люблю наступать на грабли на которые уже наступали другие и не раз. И не люблю заниматься велосипедостроительством. Также не люблю писать код ради кода.
Можно все самому с нуля написать. Но зачем? Также хорошо чтобы и другие разработчики понимали что написано, а здесь важен общий язык (привет очень "одинаковому" пониманию термина "модель" в пхп сообществе).

А насчет того что спрашиваю код готовый посмотреть - это потому что это самый лучший мануал, значительно лучше примеров оторванных от реальности. Только на объемном коде можно увидеть реальный профит каких-то паттернов, и CQRS в частности.

А так я пока не увидел ни одного вменяемого большого проекта с использованием CQRS. Я понимаю что в большинстве случаев это внутренние и закрытые разработки, а тут NDA и все такое, но если брать опенсорс, то там этого нет и нет от слова совсем.

Т.е. я то понимаю профит от разделения на рид и райт модели (юзаю уже лет 8 минимум) и понимаю профит от разделения на команды и квери. Но т.к. все было самопальное (ридмодели POPO, райт модели различные варианты ормов), то интересно как у кого все это реализовано вместе (включая квери, комманд бас и т.д.)
 

AmdY

Пью пиво
Команда форума
Пример реального CQSR из моей практики. Делали апишку, которая проверяет разрешения на работу и допуски на оборудование. В момент проверки работник находится в шахте и там нет интернета, при выходе на наружность интернет есть и клиент посылает все накопленные данные на проверку. Проверка данных требует запросы к чужим апи и может занимать сутки, а то и более.
Вот тут нужен реальное, а не силиконовое разделение.

Во первых, UUID и остальные данные генерирует на клиенте.
На поверхности эти dto-шки уходят на сервер и не ждут ответа даже с валидацией реквеста, данных много. проверок много, интернет не надёжный.
Затем клиент делает запрос (у него же есть предварительные dto) по uuid. В ответ получает отдно из трех
1. данные не валидны
2. данные валидны, мы их сохранили, но пока допуски не проверили, приди через часик.
3. всё ок, мы проверили карту, работник допущен или нет.

В силиконовом примере Аделя, всё происходит синхронно, валидируется реквест, генерируется ююид, создаётся дтошка, персистятся данные, на клиент возвращаются данные об ошибках или об успешном завершении, да ещё с ююидом. Мы в такие моменты при write не играли в CQSR и искуственные ограничение, а возвращали для удобства клиенту полноценную Read model, чтобы он за ней отдельным запросом не бегал.
 

Вурдалак

Продвинутый новичок
@AmdY с чего ты взял, что CQRS как-то мешает возвращать read model в API?

В силиконовом примере Аделя, всё происходит синхронно, валидируется реквест, генерируется ююид, создаётся дтошка, персистятся данные
А это к чему и какое отношение это имеет к CQRS?

Очень напоминает риторику политиков, которые не понимают суть проблемы, но благодаря пафосным речевым оборотам, различным софистическим приёмам набирают голоса.

CQRS и твои требования совершенно друг другу не противоречат. Типичное соломенное чучело.

@AnrDaemon @Yoskaldyr вы как голосовавшие за пост @AmdY, что именно вас так впечатлило? Человек несёт полнейшую ахинею.
 
Последнее редактирование:

Yoskaldyr

"Спамер"
Партнер клуба
@AnrDaemon @Yoskaldyr вы как голосовавшие за пост @AmdY, что именно вас так впечатлило?
Лайки - вещь которую можно понять неоднозначно. Я поставил лайк ,потому что это был первый реальный а не силиконовый пример применения CQRS (ну или по крайней мере как его понимает @AmdY, и что там фактически никто кроме него не скажет). Я с самого начала темы спрашивал насчет реальный примеров и вообще никто кроме синтетики не привел никакого кода или реальных кейсов. Если исходить из публичной информации то можно сделать вывод что CQRS в пхп это из разряда Неуловимого Джо, который нахрен никому не нужен (хотя я понимаю что это не так).
 

Вурдалак

Продвинутый новичок
CQRS — это про разные модели для записи и чтения.
CQRS — не про то что возвращает command bus. Ты можешь иметь обычные FooService->bar(), там будет обращение к write model и всё.

Разделение на write/read models обычно элементарное и напрашивается само собой. Ну, например, полю «age» как правило нечего делать в сущности User, потому что это вычисляемое значение. Соответственно, не должно быть команд, которые меняют возраст: это бессмыслица. Различных вычисляемых значений может быть достаточно много и не все они будут вообще похожи на исходную сущность. Далее нужно как-то гарантировать доставку изменений до read models, некоторые можно сохранять в той же транзакции (чего уж там), для некоторых сделать (полу-) event sourcing с гарантированной доставкой через какую-нибудь Kafka, можно еще заюзать какой-нибудь Debezium, но это всё технические детали.

CQRS в этом смысле прост: огромное количество моделей выглядят по-разному при записи и чтении. write-model — пачка методов, возвращающих void. read-model — обычная DTO с геттерами, но и она может иметь некоторую логику для вывода.

write-model может при релизе своих событий возвращать эти самые события, в некоторых случаях их достаточно для ответа (тот же id-шник, результат действия в игре «Морской бой» [попал/не попал/ранил/утопил/победил] и т.д. и т.п., каждый раз возвращать всё игровое поле необязательно — клиент не может запомнить какие поля уже отыграли?).

Но можно и read-model возвращать в API, но тогда зачастую нужно гарантировать ее актуальность, сделать синхронный запрос, никаких противоречий с CQRS тут нет. Можно на тот же GraphQL посмотреть — там из мутаций можно вернуть какую угодно read model (если в схеме это укажешь).

@AmdY же рассказывает, что он сделал запрос, а потом опрашивал сервер на предмет его успешности.
Когда @AmdY сделал первый запрос на изменение, он зафиксировал транзакцию (иначе это просто будет через раз работать). Он выполнил вполне себе команду. Просто она может называться как-то вроде «RequestSmth» или классический пример с PlaceOrder (в его примере команда может называться «отчитаться при выходе из шахты», «покинуть шахту» или еще миллионом способов; и это не предполагает, что эта команда будет синхронно высчитывать какую-то статистику — с чего бы? Мы фиксируем факт завершения работы шахтера и всё). Мы размещаем по сути заявку на заказ, но там еще будет череда проверок и промежуточных транзакций (бронирование со склада и прочее).
Это называется long-living transaction и это решается через те же saga, process manager и прочие.
И никакого отношения к CQRS это не имеет.

Команда должна быть простой. Команда вносит атомарное изменение. Атомарное изменение должно быть тривиальным и, как можно догадаться, оно не должно затрагивать несколько серверов.

P.S. UUID на стороне клиента может еще служить ключом идемпотентности. Но и это не имеет никакого отношения к CQRS.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
О, а вот как ты определяешь, к чему принадлежат Read Models?
Read Models зачастую не относятся ни к какому Bounded Context (да и по идее вообще к ним, строго говоря, не относятся), но что-то же с ними надо делать. В таком небольшом примере все в кучку нормально, а если проект большой?
Мне не нравится разделение по типам классов, я предпочитаю делить по слоям и контекстам.
Тут еще что понимать под read models. Вот, например, модельки для GraphQL, которые образуют API, у нас тупо в одной папке, потому что на самом деле эти модельки — часть API. Там всё связано друг с другом (ну, граф же), и попытка разнести такие модельки по разным bounded contexts приведёт к еще большим проблемам. Нельзя делать вид, что эти независимые структуры, если они напрямую зависят друг от друга. То есть, GraphQL API — это еще один такой суррогатный bounded context.
 

AmdY

Пью пиво
Команда форума
Фейспалм.
Да, сqrs это про разные модели чтения и записи. Но про модели программирования, модели взаимодействия, а не про модели предметной области. Термин растёт из 80х годов, когда ещё не было Еванса с его DDD.
Модель чтения означает - мы получили запрос и только читаем данные, но не вносим никаких изменений.
Модель записи - мы приняли команду на изменения данные, результата изменений не жди. Чтобы получить новые значение сделает ещё один запрос на чтение, но нет гарантии что запрос на изменения уже отработал.
Такое разделение уменьшает сайд эффекты и используется много где.

Вот ты упомянул GraphQL, он как раз прекрасный пример сайд эффектов, если нарушать cqrs.
У нас есть список пользователей и сответствующий квери с полями id, name, zoneId, status
Выбрали пользователя, попали на экран пользователя.
Сверили данные, отправили их на проверку с помощью мутатора. Мы же не используем cqrs и говорим мутотору, чтобы он вернул нам значения поля status.
Тут возникает первая проблема, поле получена, информация на скрине пользователя обновлена. А на списке нет. Надо юзать реактивное программирование, подписывать юзера на изменения, он везде обновится.
Возвращаемся на список, статус поменялся. Красота, но пользвателя перевели в другую зону, zoneId поменялся, но мы это не узнаем, потому что данные изменили частично, получив их в мутаторе, а не через апиху для которой брался список пользователей.

В случае модели cqrs, ты поменял данные и знаешь что надо дёрнуть заново кверик чтобы их получить, а не брать огрызки данных из команды. При этом ещё держишь в голове, отсылка команды не значит немедленное выполнение и полученные данные могут поменяться и хорошо бы подписаться на пуш уведомления.
 

Вурдалак

Продвинутый новичок
Термин растёт из 80х годов, когда ещё не было Еванса с его DDD.
Здравствуйте. Термин был введен Грегом Янгом в прошлом десятилетии.

Да, сqrs это про разные модели чтения и записи. Но про модели программирования, модели взаимодействия, а не про модели предметной области.
At its heart is the notion that you can use a different model to update information than the model you use to read information.
...
If you're using a Domain Model, then this is usually the conceptual representation of the domain.
...
The change that CQRS introduces is to split that conceptual model into separate models for update and display, which it refers to as Command and Query respectively following the vocabulary of CommandQuerySeparation. The rationale is that for many problems, particularly in more complicated domains, having the same conceptual model for commands and queries leads to a more complex model that does neither well.
...
By separate models we most commonly mean different object models, probably running in different logical processes, perhaps on separate hardware.
Обрати внимание, что там упоминается CQS в контексте «используя лексикон CQS». И всё.

И ты, как многие, просто путаешь и смешиваешь эти два термина.

Модель записи - мы приняли команду на изменения данные, результата изменений не жди. Чтобы получить новые значение сделает ещё один запрос на чтение, но нет гарантии что запрос на изменения уже отработал.
Это в Библии написано или в Коране? Команды вполне себе синхронны, и могут возвращать ошибки (явно или через исключения), события (потому что это прямой результат действия команды) и т.д. Это естественно, это удобно. Мы должны понимать принял ли наш сигнал участник системы или нет. Опять-таки, автор термина CQRS, которым ты так ловко оперируешь, говорит сам следующее: https://twitter.com/gregyoung/status/773240442586554368?lang=en
who says commands are async in CQRS? I normally implement them as synchronous
hah there are talks I have done where I point out asynchronous commands *don't exist* you want an event.
Ты можешь лучше, если не будешь таким высокомерным.
 

AmdY

Пью пиво
Команда форума
Судя по статье, Янг не придумывал термин, а пояснял его. И там всего-то добавлена буква R, мол давайте делать cqr не в одном, а в разных сервисах, безо всяких доменных read-write моделей и тем более без команд возвращающих их.

Java:
When most people talk about CQRS they are really speaking about applying the CQRS pattern to the object that represents the service boundary of the application. Consider the following pseudo-code service definition.

CustomerService

void MakeCustomerPreferred(CustomerId)
Customer GetCustomer(CustomerId)
CustomerSet GetCustomersWithName(Name)
CustomerSet GetPreferredCustomers()
void ChangeCustomerLocale(CustomerId, NewLocale)
void CreateCustomer(Customer)
void EditCustomerDetails(CustomerDetails)

 

Applying CQRS on this would result in two services


CustomerWriteService

void MakeCustomerPreferred(CustomerId)
void ChangeCustomerLocale(CustomerId, NewLocale)
void CreateCustomer(Customer)
void EditCustomerDetails(CustomerDetails)

CustomerReadService

Customer GetCustomer(CustomerId)
CustomerSet GetCustomersWithName(Name)
CustomerSet GetPreferredCustomers()

That is it. That is the entirety of the CQRS pattern. There is nothing more to it than that…
 

Adelf

Administrator
Команда форума
Вот ты упомянул GraphQL, он как раз прекрасный пример сайд эффектов, если нарушать cqrs.
У нас есть список пользователей и сответствующий квери с полями id, name, zoneId, status
Выбрали пользователя, попали на экран пользователя.
Сверили данные, отправили их на проверку с помощью мутатора. Мы же не используем cqrs и говорим мутотору, чтобы он вернул нам значения поля status.
Тут возникает первая проблема, поле получена, информация на скрине пользователя обновлена. А на списке нет. Надо юзать реактивное программирование, подписывать юзера на изменения, он везде обновится.
Возвращаемся на список, статус поменялся. Красота, но пользвателя перевели в другую зону, zoneId поменялся, но мы это не узнаем, потому что данные изменили частично, получив их в мутаторе, а не через апиху для которой брался список пользователей.
Ты лишь описал неправильный дизайн фронтэнда :)
write запрос может возвращать произошедшие эвенты(одна из идей пропагандируемых Вурдалаком), которые и являются нотификацией об изменениях, про которые ты говоришь.
Идея CQRS в том числе и о разных доменах для чтения и записи и это одно из самых важных вещей, которые надо там понять. То, что ты привел пример про сервисы разные из Янга - это лишь поверхность айсберга, потому что этот класс Customer будет перегружен ответственностью, если он конечно не просто CRUD-моделька.
Клиентам CustomerReadService нужно будет от него одно, а клиентам CustomerWriteService - совершенно другое. даже почти не пересекаются эти вещи.

P.S. да и вообще пример какой-то странный. Эти сервисы анемичные чтоли? Это вот смущает - void CreateCustomer(Customer) . Немного размазывается ответственность этого сервиса...
 

Yoskaldyr

"Спамер"
Партнер клуба
@AmdY Кстати спасибо за комментарий Янга. Погуглил полный вариант текста...

Это просто типичный пример изменения терминов со временем в сообществе программистов. Кто-то что-то придумает, сформулирует, потом кто-то в сообществе неправильно это поймет или поймет правильно, но расширит концепцию (не обязательно в худшую сторону), но в обоих случаях это уже будет не то, что было в самом начале придумано/сформулировано первоначальным автором. И эта модифицированная концепция уже продвигается в массы, как концепция первоначального автора. Отсюда и все недопонимания и размытия концепции и как результат срачи на форумах :)
 
Сверху