Command bus и CQRS

Adelf

Administrator
Команда форума
@Вурдалак зачем его юзаешь?
Эти CreateUserCommand & CreateUserCommandHandler?
В чем профит?
Я видел разок, где предлагали делать middleware в котором валидировали команду.

PHP:
class CreateUserCommand
{
    /**
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    public function getEmail(): string;
}
А миддлвар валидирует все эти DTO перед выполнением хендлером, освобождая того от этой работы.
В чем еще?
 

Adelf

Administrator
Команда форума
The command bus itself is really easy to decorate with extra behaviors, like locking or database transactions so it’s very easy to extend with plugins.
Чот мне не очень нравится эта идея... это может делать некоторые транзакции сильно длиннее чем нужно.
 

Вурдалак

Продвинутый новичок
Причины примерно такие:

1. Это явным образом разграничивает команды и запросы, с FooService трудно понять где команды, где — запросы.
2. Иногда в рамках одного контроллера/саги/CLI-команды нужны команды к разным aggregate root'ам и инжектить несколько сервисов — ну, такое себе, это раздражает (особенно, когда инжектить будешь и в контроллер, и в CLI-команду и в Behat-context одновременно).
3. Позволяет делать общие для команд вещи (например, пускать события по event bus, собирать события по всем вложенным командам, etc.).
4. Создаёт точку расширения для добавления логгирования (в файл, например) и некоторых подобных штук (статистика в Pinba/newrelic, например; удачные/неудачные команды).
5. Единый интерфейс для команд: с command id, без; требование context-объекта (что-то типа https://beberlei.de/2017/03/12/explicit_global_state_with_context_objects.html), возвращаемые значения (события, ошибки, etc.).

Транзакции/локи я давно уже не делаю с помощью [декораторов] command bus, это всё в репозитории (даже если в умных книжках говорят, что так не стоит делать).

А класс-handler обычно один на один aggregate root, просто несколько методов handle* с соответствующей командой в type hinting.
 

Вурдалак

Продвинутый новичок
Я видел разок, где предлагали делать middleware в котором валидировали команду.
PHP:
class CreateUserCommand
{
    /**
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    public function getEmail(): string;
}
Это мне не нравится. Как я уже неоднократно говорил, внутри модели должны быть жёсткие assertion'ы. Там нужно тупо падать, если вместо email передали мусор.
А вот та точка, откуда этот email пришёл — да, там можно через Symfony Validator проверить так email, но команда для command bus — это не то, что напрямую передаёт клиент, он передаёт какую-то команду на уровне API, которая может смэппиться на аналогичную для command bus. Но что если мы хотим подставлять email не из $httpRequest, а из $loggedInUser->getEmail()? И вдруг там будет мусор — мы будем конечному пользователю показывать ошибку «Email введен в неверном формате», даже если поле email'а на форме отсутствует вовсе?
 

Adelf

Administrator
Команда форума
Т.е. все хендлеры возвращают результат единого класса? CommandHandlerResult?

А класс-handler обычно один на один aggregate root, просто несколько методов handle* с соответствующей командой в type hinting.
А они не большими получаются? Или у тебя там мало работы с инфраструктурой? А то мои хендлеры(по классу на экшен) были иногда по 100-200 строк из-за всяких апи-колов, которых нельзя было отложить.

Это мне не нравится.
Т.е. лучше уже в команде VO? Мне вот так нравится:
PHP:
class CreateUserCommand
{
    public function getEmail(): Email;
}
 

Вурдалак

Продвинутый новичок
Т.е. все хендлеры возвращают результат единого класса? CommandHandlerResult?
Типа того.

А они не большими получаются?
Хендлеры обычно только с репозиторием у меня работают. Всякие «апи-коллы» — это звучит, как немного другой контекст (возможно, который над текущим).

Т.е. лучше уже в команде VO? Мне вот так нравится:
PHP:
class CreateUserCommand
{
    public function getEmail(): Email;
}
Да это не столь принципиально в этом вопросе, я имел в виду, что клиентским валидатором проверять напрямую команду не совсем правильно. Пусть проверяет какой-то там RegisterUserRequest.

P.S. Почему ты говоришь «create user» — в Бога играешь? :)
 

Adelf

Administrator
Команда форума
Всякие «апи-коллы» — это звучит, как немного другой контекст (возможно, который над текущим).
Заставило задуматься. А картинку к посту в S3 загрузит Web часть? Не хендлер? А проверит ее на всякий адалт или насилие? Да, у меня последний проект был контентный, у меня мозг деформирован немного :)
Другой колл, например, посмотреть нет ли в тексте публикации мата(пишут обычные юзеры, а страдает в случае чего сайт. надо проверять).
 

Вурдалак

Продвинутый новичок
Ну смотря что под юзером разуметь, но да. если разуметь другое, то надо называть Account хотя бы.
Я про глагол: не стоит CRUD использовать, ты не сотворяешь пользователя, ты его регистрируешь, его мама с папой сотворили.

Заставило задуматься. А картинку к посту в S3 загрузит Web часть? Не хендлер? А проверит ее на всякий адалт или насилие?
Можно просто сделать обычный сервис PhotoUploader, который в application, который грузит куда нужно, а потом вызывает command bus с UploadPhoto, где по сути будет просто фиксация в базе и генерация событий.
 

Adelf

Administrator
Команда форума
Я про глагол: не стоит CRUD использовать, ты не сотворяешь пользователя, ты его регистрируешь, его мама с папой сотворили.
Я в курсе. Если create, то CreateAccount надо.

Можно просто сделать обычный сервис PhotoUploader, который в application, который грузит куда нужно, а потом вызывает command bus с UploadPhoto, где по сути будет просто фиксация в базе и генерация событий.
Не. Фотка - это просто картинка. В домене, от этой фотки останется только url. Публикацию без картинки создать нельзя. Соответственно, надо в процессе создания Post загружать картинку...
Или ты говоришь совсем отдельно загрузить фотку, другой командой...
PHP:
class PostController
{
public function create($request)
{
$uploadResult = $this->commandBus->execute(new UploadPhotoCommand(...));
if($uploadResult is ok)
{
$this->commandBus->execute(new CreatePostCommand(...));
...
}
?
 

Вурдалак

Продвинутый новичок
Как так, прямо у меня в базе данных? куда смотрела охрана ? :O
В базе создаётся запись в таблице, а не пользователь.

Я в курсе. Если create, то CreateAccount надо.
А, да, кстати.

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

Вурдалак

Продвинутый новичок
В домене, от этой фотки останется только url. Публикацию без картинки создать нельзя.
Ну значит достаточно просто сделать обязательным параметром URL фотки. А уж битый это URL или нет, картинка там или видео — это не головная боль модели Post, на этом уровне абстракции мы можем наплевать на такие вещи. Ты ведь когда unit-тест для Post будешь писать не будешь реально картинку куда-то загружать — этой модели наплевать.
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
а дайте глянуть на более-менее полноценный пример кода
Адель, где предлагали?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
мне как-то всегда проще с чистым кодом - не надо догадываться, что было в голове у автора, когда он писал )))
 

Yoskaldyr

"Спамер"
Партнер клуба
подниму старую тему :)
Думал создать новую, т.к. по CQRS/CQS обсуждение есть, но отдельной темы нет.
Но с другой стороны данная тема ближе всего (хотя тут только комманд бас обсуждается)

@Adelf к чему все-таки пришел в результате? Мне тоже не совсем нравится многословность использования отдельных пар (или более) CreateUserCommand & CreateUserCommandHandle для каждой фактической команды. Учитывая что CreateUserCommand в 99% случаев простой VO и отличается от других классов команд только конструктором и зависимостями. Т.е. фактически классы команд (не хандлеров) - это почти дублирование кода. Или используешь один унифицированный класс команды (или минимальный набор классов команд), куда параметры передаются через контекст? Это если я правильно понял это:
5. Единый интерфейс для команд: с command id, без; требование context-объекта (что-то типа https://beberlei.de/2017/03/12/explicit_global_state_with_context_objects.html), возвращаемые значения (события, ошибки, etc.).
Просто если говорить об упрощенном CQS (без комманд баса и разделения на команду и хандлер), то кода получается в разы меньше и это проще использовать c другими одаренными разработчиками. Вот я и думаю как сделать с точки зрения кода, чтобы и писать было немного как при CQS, но был более менее полноценный CQRS.
 
Сверху