Command bus и CQRS

fixxxer

К.О.
Партнер клуба
Сам по себе CQRS, да, тупой как буратино. С tutorial-driven подходом - как в Симфони с anemic models + services - даже и не очень понятно, ну разнесли один класс на два, "и что".
Но если начать с DDD и применить CQRS поверх DDD, там уже намного более интересные выводы.
 

Yoskaldyr

"Спамер"
Партнер клуба
@fixxxer Вот поэтому я с самого начала спрашивал насчет примеров. Но по видимому их в открытых источниках - нет.

Я понимаю что пример @Adelf синтетический, но он уж очень синтетический даже для примера и работа с моделями там сверх костыльная.

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

Yoskaldyr

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

@Adelf Насчет рид моделей (проекций) у них понравилось - ничего лишнего и понятно зачем это все надо. Также использование пейлоада и активное использование кверибас в командах (хотя многие пишут что типа так нельзя делать, но лично я не представляю как это можно сделать в большом приложении)
 

Вурдалак

Продвинутый новичок
Судя по статье, Янг не придумывал термин, а пояснял его.
Он вводил именно новый термин. Потому что это семантически уже другой уровень.
Я не буду говорить CQS вместо CQRS и обратно, потому что это ни разу не взаимозаменяемые термины. Поэтому я говорю — это новый термин.
CQS, например, про методы. Обычные процедуры. Низкий уровень. Ниже только команды asm. И он требует, чтобы метод не возвращал ничего.
CQRS же про модели. Он ничего не говорит про методы. Метод того же Command Bus может что-то менять и возвращать в ответ. Прямо противоречит CQS. Почему же тогда так можно? Да потому что CQRS и CQS — это разные вещи, семантически.
Чтобы понимать друг друга, нужно использовать точные понятия. Особенно в профессиональной среде. Потому что мы как раз те люди, которые должны понимать разницу.

Но я на месте Грега Янга, тоже бы был очень аккуратным и не таким громким в заявлениях. Я вижу его заслуги. Он старается держаться скромнее и говорить «да, вот так все просто». Потому что сейчас легко обосраться с новыми терминами. Вот был какой-то Flux, не помню деталей, но они пафосно заявляли про какой-то новую архитектуру, использовали какие-то свои термины вперемешку с buzz word MVC, чтобы вообще было максимально расплывчато. Потом им вежливо объяснили, что ничего нового-то, это CQRS, ребята.

Про методы (CQS) — ну, это интересно звучало, но тут же находились контр-примеры вроде стека и метода pop(). Поэтому принцип был не очень консистентным, трудно было объяснить что не так и в чем реальный профит.

не придумывал термин, а пояснял его. И там всего-то добавлена буква R
Он «всего лишь» пояснял, добавил букву. Ведь все и так было очевидно, да?
На самом деле он сделал гениальную вещь — он всем показал, что похожий принцип работает на другом семантическом уровне — когда не какие-то процедуры, методы, а модели. Там уже появляется огромное количество профита, именно CQRS расставляет на свои места многие «серые пятна» в архитектуре: часто чуть ли не годами было ощущение, что что-то не так. Но никто не мог это сформулировать. А он сделал и простыми словами без особого пафоса.

Нам кажется, что все было и так ясно из-за когнитивных искажений вроде https://ru.wikipedia.org/wiki/Ошибка_хайндсайта

Сейчас ты пытаешься сохранить лицо, продолжая говорить чушь про то, что в CQRS методы должны ничего не возвращать. Наверное, в CQRS запрещены стеки, инкременты с возвратом. А почему? Потому что так правильно, еще с 80-х знали.

Ты можешь иногда использовать выражения типа «мне кажется», «по-моему», «мне показалось», «вроде бы». Они сглаживают сообщения и если ты оказался не прав, тебе будет намного проще сказать «уф, я заблуждался». А когда ты пытаешься показать, что все вы идиоты, ваши ДДД, СКРС, давным-давно уже все знали, что раньше трава была зеленее — ты теряешь всякий авторитет. Это инфантильно.
 

Вурдалак

Продвинутый новичок
Книга Эванса по DDD тоже на самом деле «всего лишь» каталогизирует некоторые паттерны, витавшие в воздухе в то время, дает им имена и рассказывает на примерах, пытаясь передать саму суть.

Ведь что там особенного?

Bounded context — не нужно иметь несвязанную логику из разных контекстов (пакетов/модулей/подпрограмм) в одном месте. Нужно разделять на самодостаточные контексты, внутри которых будет высокая связность (cohesion). Но чем это отличается от SRP? Просто другой семантический уровень, а суть та же.

А Ubiquitous language? Если разработчики и специалисты будут использовать более точные и друг другу понятные термины, то они разработчики будут точнее делать модели, где будет меньше обидных багов из-за недопонимания. Это кажется очевидным, да?

Там просто набор «очевидных» вещей куда не глянь. И во многом поэтому я иногда говорю, что это просто ООП для моделей. Но увидеть эти «очевидные» вещи, собрать у себя в голове полную картину — это сложно.

А, ну и вдогонку. У тебя классический https://ru.wikipedia.org/wiki/Эффект_Даннинга_—_Крюгера
Для тебя все просто.
Все понятно.
А по сути — ты не знаешь ничего.
 

AmdY

Пью пиво
Команда форума
Я ссылался на статью того самого Янга, а ты переходишь на личности и эффект Даннинга-Крюгера.

Вот ещё из Янга про read-write модели
CQRS is not eventual consistency, it is not eventing, it is not messaging, it is not having separated models for reading and writing, nor is it using event sourcing.
И опять же он говорит что определения комманды и квери такое же как и в CQS
the same definition that is used by Meyer in Command and Query Separation, a command is any method that mutates state and a query is any method that returns a value
 

Yoskaldyr

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

Вурдалак

Продвинутый новичок
Согласен, в соответствии с тем описанием, что он даёт в феврале 2010, я не прав, когда употребляю слово «модель»: http://codebetter.com/gregyoung/2010/02/15/cqrs-is-more-work-because-of-the-read-model/

Я посмотрел на эволюцию его взглядов относительно этого паттерна.

Я нашел его пост за год то того поста, на который ты ссылаешься (August, 2009): http://codebetter.com/gregyoung/2009/08/13/command-query-separation/
Судя по всему, тогда вообще он использовал термин CQS и это вызывало недовольство людей:
One thing that has come up with many people in learning it is that they get confused between CQS [Meyer] and CQS [Architecture or DDD]. As such many have called for us to rename the latter to something different as although it is very similar to CQS [Meyer] they find it to be quite different. I however disagree with this let’s look at our definitions.

Meyer:
Separate command methods that change state from query methods that read state.

Current:
Separate command messages that change state from query messages that read state.

The reader should notice that these two are nearly identical, the principle has remained the same, only the definitions of what commands and queries are has changed.
И тут я не могу не согласиться с людьми, которые были не довольны термином: у Мейера речь шла о методах. О низкоуровневой вещи. Метод и команда, как сам говорит Янг имеют разные определения. И здесь, как мне кажется он делает ошибку, пытаясь использовать один и тот же термин. Банально потому что команда может что-то возвращать и это прямо противоречит тому, о чем говорит исходное определение Мейера.

Потом судя по общению с Udi Dahan (https://books.google.ru/books?id=MFnTBgAAQBAJ&pg=PA17&lpg=PA17&dq=http://codebetter.com/gregyoung/2009/08/13/command-query-separation/&source=bl&ots=LLpEV0fJc2&sig=ACfU3U3d4DULZuMCRuYMMzeC6mvQvZ8N9g&hl=en&sa=X&ved=2ahUKEwiA7qzR8dzkAhXs-ioKHcs_DMsQ6AEwAXoECAkQAQ#v=onepage&q=http://codebetter.com/gregyoung/2009/08/13/command-query-separation/&f=false) он всё-таки согласился, что нужно ввести новый термин:

Greg Young and Udi Dahan reached the conclusion of refining CQS into something called Command Query Responsibility Segregation
...
Going beyond the separation of read and write, CQRS is really abount creaing the models needed for the different aspects of your solution
Также https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
Command and Query Responsibility Segregation was originally considered just to be an extension of this concept. For a long time it was discussed simply as CQS at a higher level. Eventually after much confusion between the two concepts it was correctly deemed to be a different pattern.
— здесь он уже сам подтверждает, что пришлось разделить это на два концепта.

И еще годом позже (2011) Fowler уже использует слово «модель»: https://martinfowler.com/bliki/CQRS.html

Но в чем принципиальная разница между CQS и CQRS, которая казалась Янгу в 2010 незначительной?
Команда уже не метод, это нечто более высокоуровневое. Хотя может быть и методом.
Но принципиально здесь то, что возвращаемое значение от команды теперь не является запросом.
Я не делаю запрос, когда я выполняю commandBus->handle(...).
Я выполняю команду.

И это об этом говорит и сам Янг про one way commands don't exist, про то можно можно возвращать ошибки и прочее.

Вот еще парочка похожих мнений:
Not really. Without violating any principles, a command can safely return the following data:

Execution result - success / failure;
Error messages or validation errors, in case of a failure;
The aggregate’s new version number, in case of success;

Лишний раз доказывает, что нужно более точно говорить о том, что ты имеешь в виду, а не использовать один и тот же термин в разных контекстах, потому что это приводит вот к подобным серьезным заблуждениям:
Модель записи - мы приняли команду на изменения данные, результата изменений не жди. Чтобы получить новые значение сделает ещё один запрос на чтение, но нет гарантии что запрос на изменения уже отработал.
 

Вурдалак

Продвинутый новичок
Согласен, в соответствии с тем описанием, что он даёт в феврале 2010, я не прав, когда употребляю слово «модель»: http://codebetter.com/gregyoung/2010/02/15/cqrs-is-more-work-because-of-the-read-model/
Случайно вставил не ту ссылку: http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/


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

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

AmdY

Пью пиво
Команда форума
Нет, это Скотный двор с его "Все животные равны. Но некоторые животные равны более, чем другие". Когда изначальное утверждение изменили до уровня антипатерна.
Считаю read-write модели таковыми, т.к. это ведёт в дублированию кода и трудностям поддержки двух кодовых баз, которые во многом пересекаются. Предпочитаю делить модели по контексту, когда между ними действительно много отличий, а чтение-запись не всегда является таковым.
 

Вурдалак

Продвинутый новичок
А что такое «антипаттерн»? Я в некоторых случаях стал замечен в употреблении синглтона, например (не вискаря, а того самого антипаттерна). Всё яд при определенной дозировке.
 

Вурдалак

Продвинутый новичок
Но вообще как-то странно, что ты говоришь про разделение моделей, когда вроде бы спор был вокруг может ли возвращать CommandBus->handle() что-то в ответ или нет. Вот ты хитрец.
 

AmdY

Пью пиво
Команда форума
Но вообще как-то странно, что ты говоришь про разделение моделей, когда вроде бы спор был вокруг может ли возвращать CommandBus->handle() что-то в ответ или нет. Вот ты хитрец.
Я про это даже не спорил. Для меня однозначно НЕТ, иначе это уже другой паттерн, да и наличие самого CommandBus не является обязательным. Да и в янговском CQRS, методы возвращали void, а приведённая тобой статья 16 года от другого автора всё же не канон (http://danielwhittaker.me/2016/04/20/how-to-validate-commands-in-a-cqrs-application/ ).
 

Вурдалак

Продвинутый новичок
Я про это даже не спорил. Для меня однозначно НЕТ, иначе это уже другой паттерн, да и наличие самого CommandBus не является обязательным. Да и в янговском CQRS, методы возвращали void, а приведённая тобой статья 16 года от другого автора всё же не канон (http://danielwhittaker.me/2016/04/20/how-to-validate-commands-in-a-cqrs-application/ ).
Давай перейдём от аргументов «канон/неканон» к реальным, мы не фанфик по Гарри Поттеру обсуждаем.
У Грега в примере все методы возвращают void.
Следует ли из этого, что все методы во write service должны возвращать void?
Нет, не следует.
Ты сделал какое-то странное обобщение.

Должна быть какая-то аргументация зачем нужно так, а не иначе с точки зрения здравого смысла, а не потому что так когда-то кто-то написал. Грег — не Джоан Роулинг.

Если тебе нравится библейский подход, то вот http://codebetter.com/gregyoung/2009/08/13/command-query-separation/
Meyer:
Separate command methods that change state from query methods that read state.

Current:
Separate command messages that change state from query messages that read state.
— если бы в CQRS по-прежнему речь шла о методах, то command method он не стал бы заменять на command message.

Или с другой стороны: у тебя CommandBus ошибки тоже не возвращает? Не bool, ни исключения? А если bool или исключения есть, то чем это не command result? В каком-нибудь Go нет исключений, там нужно именно возвращать, т.е. это языковый нюанс.
 

AmdY

Пью пиво
Команда форума
При чём здесь Гарри Поттер и библия? Если ты считает аргумент вроде канон не реальным, то зачем вообще обсуждать, всегда найдётся Дэниэл со своим видением, другая статья где один из терминов сформулирован возможно по другому, в том числе термины другого автора.

Для меня канонична статья http://danielwhittaker.me/2016/04/20/how-to-validate-commands-in-a-cqrs-application/ . В ней дано определение, приведены примеры и сказано что это всё (There is nothing more to it than that…). На эту же статью ссылается Фаулер, а ему в поисках истоков термина доверяю больше, чем себе.
а
Если хочешь таскать теории из других неканоничных источников, которые подтверждают твою точку зрения - пожалуйста, но без меня, и без навешивания на меня своих ярлыков.
 

Вурдалак

Продвинутый новичок
При чём здесь Гарри Поттер и библия?
Единственный твой аргумент — https://ru.wikipedia.org/wiki/Argumentum_ad_verecundiam
Что является логической ошибкой.
Своего мнения у тебя нет — ты просто выбираешь кого слушаться.

Даже если взять статью http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/ — я говорю, что интерпретировать ее можно по-разному, но тебе неприятно это слышать, ты игнорируешь мои комментарии, где я, например, прошу тебя объяснить почему ты делаешь обобщение, что все write services по какой-то причине должны быть void. В статье Грег нет никакого определения. Сухого, формального. Есть «вот так вот». Там нет четких границ. Так почему всегда должно быть void? Разве Грег дал определение в стиле «любой write service должен удовлетворять...»? Он дал пример. Частный случай.

Всегда нужно понимать зачем делается именно так, а не иначе. «Потому что канон» — это мнение верующих людей.
 

Вурдалак

Продвинутый новичок
И я бы реально не спорил, если бы Грег дал сухое определение, в котором он бы сказал, что CQRS подразумевает, что результат команды обязан быть получен с помощью Query, и после он бы не стал корректировать это определение. В таком случае я бы не стал использовать термин «CQRS», и использовал бы какое-то другое понятие. Я не буду слепо ставить void, потому что так кто-то сказал; должно быть объяснение, которое реально убедит меня.

А что касается смены позиции под наплывом новых фактов — это абсолютно здоровое поведение. В научной среде это ценится.
 

alexion

Новичок
А у кого-нибудь есть логичное объяснение использования такой схемы
PHP:
class CreateUserCommand
{
    private $email;
    private $name;
   
    public function __construct (string $email, string $name)
    {
        $this->email = new Email($email);
        $this->name = new Name($name);
    }
}
вместо такой
PHP:
class CreateUserCommand
{
    private $email;
    private $name;
   
    public function __construct (Email $email, Name $name)
    {
        $this->email = $email;
        $this->name = $name;
    }
}
Постоянно встречаю в проектах оба варианта, кроме как вкусовщина, логического объявления для себя не нашел)) Считаю передачу в конструктор уже готовых VO более коротким вариантом.
 

AnrDaemon

Продвинутый новичок
А у кого-нибудь есть логичное объяснение использования такой схемы
Типично неудачная попытка скрестить ежа с ужом (DTO с его фабрикой).
Не надо так делать.

Хороший слайдшар на тему
 
Сверху