Exceptions

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@Вурдалак, это разные понятия и разные проблемы.
Интернет-банкинг - это CA-архитектура по CAP-классификации, старый модемный клиент-банк с обменом пакетами - AP, а форум - CP. Если дебилы сделали интернет-банкинг по CP-архитектуре с тонким клиентом без обработки разрывов - это никак не связано с обработкой ошибок.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
@Вурдалак, это разные понятия и разные проблемы.
Какие именно понятия разные? Я привёл пример проблемы с несоблюдением идемпотентности. Обобщил её тут:
Если обобщить, то у нас есть некая команда и список событий, которые снова не возникнут (нельзя убить убитого человека), но хочется видеть в ответе.
Если вернуться к «Post already published», то вместо этой ошибки я бы хотел видеть те же события, что были при публикации. Например, PostWasPublished { ... }, PostWasSentForPreModeration { ... }.
Если пост может по какой-то внутренней логике то отправляться на премодерацию, то нет (т.е. событие `PostWasSentForPreModeration` опционально), то возникнет вопрос как ты собираешься ответить в API, что пост отправился на модерацию: лезть в query service?
А также как ты собираешься сообщить клиенту id поста, который был создан в предыдущем запросе?

Ну, то есть, чтобы было яснее:
1. Клиент делает запрос POST /publishPost {"body": "blah-blah"}. Пост создаётся и в API уходит ответ {"post-id": 42, "moderation": true}, но ответ получить не успеет.
2. Клиент (программа) делает повторный запрос POST /publishPost {"body": "blah-blah"}. Она должна получить ровно то же самое: {"post-id": 42, "moderation": true}.

А откуда мы возьмём этот 42 во втором пункте? Нужен какой-то одинаковый ключ идемпотентности от клиента в обоих запросах:
Код:
POST /publishPost {"command-id": "1071fab0-b9e3-11e8-96f8-529269fb1459", "body": "blah-blah"}
Я понимаю, что тебе нравится тема банков, тонких/толстых клиентов и CAP, а мне вот нравится говорить про идемпотентность. Но проблема того банка и публикации поста имеет много схожего, просто присмотрись.
 

Adelf

Administrator
Команда форума
Мм. Я понимаю, что нельзя валидировать VO-обьектами :) Я лишь привел пример, что говорим не об этом.

Про Publish post.
Есть пост. И он изначально неопубликован.
И какой-нибудь редактор может нажать Опубликовать, чтобы сделать его публичным. Т.е. это не создание поста. И запрос выглядит так:
Код:
POST /publishPost {"id": "42"}
Но я понимаю, что видимо нормальным считается ответить "Ok" даже если он уже опубликован до этого. Не знаю. Надо обдумать :)
 

Вурдалак

Продвинутый новичок
Но я понимаю, что видимо нормальным считается ответить "Ok" даже если он уже опубликован до этого. Не знаю. Надо обдумать :)
OK. А что если хочется возвращать в ответе имя редактора, что только что опередил текущего? Можно лезть в query service, а можно тут же из события получить информацию, что PostWasAlreadyPublished { postId: 42, editorId: 100500 }.

То есть, к чему себя ограничивать сухими «OK» и «not OK»?
 

Adelf

Administrator
Команда форума
Я так понимаю, мы лишь расходимся в том, что из себя представляет PostWasAlreadyPublished: событие или исключение.
Да. Но ты уже привел один аргумент, который меня почти переубедил. Когда есть выделенный домен и Post::publish or User::ban то они могут быть использованы не только в PublishPostCommandHandler или BanUserCommandHandler но и в других местах, внутри домена например. И эксепшены в итоге делают грязь. Чтобы про них не забыли надо их делать checked. А это вынудит проверять их в тех самых PublishPostCommandHandler или BanUserCommandHandler.
Возвращать UserBanResult немного нудно и многословно, но в итоге это может дать бОльшую свободу. Надо попробовать, что и как. Событие - ты ведь имеешь ввиду просто результат экшена, а не какие-то вещи как доменные эвенты?

Я бы, кстати, ответил 409 Conflict на уровне HTTP API в этом случае.
А этот большой пласт кода, который будет превращать результат выполнения экшена в HTTP ответ... не слишком ли оверинжинииринг? Там и сам ответ экшена должен быть удобным для этого... но это такое. Уже мелочи.
 

Вурдалак

Продвинутый новичок
Событие - ты ведь имеешь ввиду просто результат экшена, а не какие-то вещи как доменные эвенты?
Я говорю именно про domain events, которые я записываю внутри сущности.

А этот большой пласт кода, который будет превращать результат выполнения экшена в HTTP ответ... не слишком ли оверинжинииринг? Там и сам ответ экшена должен быть удобным для этого... но это такое. Уже мелочи.
Я не понял твою мысль. Но достаточно странно отвечать 200 OK и при этом говорить «Ну, чувак, твои изменения не сохранены: вон тот редактор успел быстрее».
Ведь когда ты меняешь issue в каком-нибудь trac/redmine/JIRA (?) параллельно с кем-то — ты же видишь ошибку, а не то, что всё OK?
При той же публикации редактор может оставлять комментарий, например. Получается, мы не сохраним комментарий, а вдруг он важный? Пусть интерфейс покажет конфликт, возможность поправить комментарий, например.
 

Вурдалак

Продвинутый новичок
Если ты имел в виду просто 400, то в принципе тоже можно (если не лучше). Там по-любому ещё какой-то свой error code будет для клиента, чтобы он точно понял причину.
Попытка мэппинга на максимально подходящий http code — не самое благодарное занятие.
 

Adelf

Administrator
Команда форума
Я говорю именно про domain events, которые я записываю внутри сущности.
А зачем нам Domain event PostWasPublishedByAnotherAuthor? Еще скажи в Event store его запишем?

Если ты имел в виду просто 400, то в принципе тоже можно (если не лучше). Там по-любому ещё какой-то свой error code будет для клиента, чтобы он точно понял причину.
Попытка мэппинга на максимально подходящий http code — не самое благодарное занятие.
Вот вот :)
 

Вурдалак

Продвинутый новичок
Еще скажи в Event store его запишем?
It depends: обычно не запишем. Но какой-нибудь CodeWasWrong тебе придётся записать: 3 раза CodeWasWrong и Code должен инвалидироваться в целях безопасности.
Тут уже дело вкуса и предпочтений: можно помимо array $events в сущности иметь array $errors, куда тоже складывать, но отдельным методом: тогда будет явное разграничение между событиями и «ошибками»: ошибки, например, в event store точно не будут записываться. А можно marker interface к событию добавить, что это Error или что это NotForPersistence (грязновато, по дело вкуса, опять-таки).

Тут я тебе идеальный вариант не подскажу: сам ищу.
 

Adelf

Administrator
Команда форума
Только сейчас я вспомнил самый простейший пример: Смена пароля. Old password and New password. Когда старый пароль не совпал... это исключение или нет? Отвечать уже необязательно... просто все эти примеры с PostAlreadyPublished.. немного искусственные были.
 
Последнее редактирование:

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Если обобщить, то у нас есть некая команда и список событий, которые снова не возникнут (нельзя убить убитого человека), но хочется видеть в ответе.
Если вернуться к «Post already published», то вместо этой ошибки я бы хотел видеть те же события, что были при публикации. Например, PostWasPublished { ... }, PostWasSentForPreModeration { ... }.
Если пост может по какой-то внутренней логике то отправляться на премодерацию, то нет (т.е. событие `PostWasSentForPreModeration` опционально), то возникнет вопрос как ты собираешься ответить в API, что пост отправился на модерацию: лезть в query service?
А также как ты собираешься сообщить клиенту id поста, который был создан в предыдущем запросе?
PostWasSentForPreModeration - это статус, свойство сущности.
Свойства сущностей никак не связаны с идемпотентностью запросов.
Надо разделять запросы на получение данных и на изменение.
INSERT не возвращает результат работы триггера, только факт изменения строк таблицы. Аналогично, ответ на запрос по изменению поста не должен содержать статус этого поста.

Ну, то есть, чтобы было яснее:
1. Клиент делает запрос POST /publishPost {"body": "blah-blah"}. Пост создаётся и в API уходит ответ {"post-id": 42, "moderation": true}, но ответ получить не успеет.
Это проблема 2pc, и к теме исключений отношения не имеет
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Только сейчас я вспомнил самый простейший пример: Смена пароля. Old password and New password. Когда старый пароль не совпал... это исключение или нет? Отвечать уже необязательно... просто все эти примеры с PostAlreadyPublished.. немного искусственные были.
давай декомпозируем
1. проверка старого пароля называется термином "аутентификация" - мы узнаем пользователя, у меня это отдельный сервис
2. проверка статуса пользователя - это авторизация, если пользователь забанен - пароль ему менять нельзя
Очевидно, неверный пароль - это ошибка аутентификации, ответ будет 401 Unauthorized
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
OK. А что если хочется возвращать в ответе имя редактора, что только что опередил текущего? Можно лезть в query service, а можно тут же из события получить информацию, что PostWasAlreadyPublished { postId: 42, editorId: 100500 }.

То есть, к чему себя ограничивать сухими «OK» и «not OK»?
CQRS - это про контракты вообще, про API распределенных систем - тоже.
Как пишет Фаулер, это не всегда удобно.
 

Adelf

Administrator
Команда форума
1. проверка старого пароля называется термином "аутентификация" - мы узнаем пользователя, у меня это отдельный сервис
Т.е. у тебя любой ранее аутентифицированный юзер может сменить пароль без дополнительного ввода старого пароля?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@Adelf, я не вижу связи между своими словами и твоим вопросом.
 

Adelf

Administrator
Команда форума
@grigori, пользователь аутентифицирован и не забанен. Поэтому можно ему дать сменить пароль без ввода старого, верно?
 

AnrDaemon

Продвинутый новичок
Нет. При попытке смены пароля аутентификация пользователя выполняется принудительно.
 
Сверху