CQRS: Значення повернення команд [закрито]


76

Здається, існує нескінченна плутанина щодо того, чи повинні команди мати чи не мати повернені значення. Я хотів би знати, чи плутанина просто тому, що учасники не вказали свого контексту чи обставин.

Плутанина

Ось приклади плутанини ...

  • Уді Дахан каже, що команди "не повертають помилки клієнту", але в тій же статті він показує схему, де команди дійсно повертають помилки клієнту.

  • Стаття Microsoft Press Store говорить "команда ... не повертає відповідь", але потім надає неоднозначне застереження:

У міру зростання досвіду бойових дій навколо CQRS, деякі практики консолідуються і, як правило, стають найкращими практиками. Частково всупереч тому, що ми щойно заявили ... сьогодні поширеною є думка, що і обробник команд, і програма повинні знати, як проходила транзакційна операція. Результати повинні бути відомі ...

Ну, обробники команд повертають значення чи ні?

Відповідь?

Беручи підказку з " Міфів про CQRS " Джиммі Богарда , я думаю, відповідь (и) на це питання залежить від того, про який програмний / контекстний "квадрант" ви говорите:

+-------------+-------------------------+-----------------+
|             | Real-time, Synchronous  |  Queued, Async  |
+-------------+-------------------------+-----------------+
| Acceptance  | Exception/return-value* | <see below>     |
| Fulfillment | return-value            | n/a             |
+-------------+-------------------------+-----------------+

Прийняття (наприклад, перевірка)

Команда "Прийняття" здебільшого стосується перевірки. Ймовірно, результати перевірки повинні надаватися абоненту синхронно, незалежно від того, чи є команда "виконання" синхронною чи в черзі.

Однак, схоже, багато практиків не ініціюють перевірку зсередини обробника команд. З того, що я бачив, це або тому, що (1) вони вже знайшли фантастичний спосіб обробити перевірку на рівні програми (тобто контролер ASP.NET MVC, що перевіряє дійсний стан за допомогою анотацій даних), або (2) архітектуру є на місці, що передбачає, що команди подаються на (поза процесом) шину або чергу. Ці останні форми асинхронності, як правило, не пропонують синхронну перевірку семантики або інтерфейсів.

Коротше кажучи, багато дизайнерів можуть вимагати, щоб обробник команд надавав результати перевірки як (синхронне) повернене значення, але вони повинні жити з обмеженнями своїх асинхронних інструментів, які вони використовують.

Виконання

Щодо "виконання" команди, клієнту, який видав команду, може знатись область_ідентифікації для нещодавно створеного запису або, можливо, інформацію про помилку - наприклад, "рахунок перевиконано".

В умовах реального часу здається, що повернене значення має найбільший сенс; винятки не повинні використовуватися для повідомлення результатів невдач, пов’язаних з бізнесом. Однак у контексті "черги" ... повернені значення, природно, не мають сенсу.

Ось тут, можливо, можна підсумувати всю плутанину:

Багато (більшість?) Практиків CQRS припускають, що вони зараз або в майбутньому включатимуть асинхронний фреймворк або платформу (шину або чергу) і, таким чином, проголошують, що обробники команд не мають повернутих значень. Однак деякі практики не мають наміру використовувати такі побудовані подіями конструкції, і тому вони схвалюють обробники команд, які (синхронно) повертають значення.

Так, наприклад, я вважаю, що синхронний (запит-відповідь) контекст передбачався, коли Джиммі Богард надав цей зразок командного інтерфейсу :

public interface ICommand<out TResult> { }

public interface ICommandHandler<in TCommand, out TResult>
    where TCommand : ICommand<TResult>
{
    TResult Handle(TCommand command);
}

Його продукт Mediatr - це, зрештою, інструмент пам'яті. Враховуючи все це, я думаю, що причина, за допомогою якої Джиммі обережно витратив час на повернення порожнього результату від команди, полягала не в тому, що "обробники команд не повинні мати повернених значень", а тому, що він просто хотів, щоб його клас Посередник мав послідовний інтерфейс:

public interface IMediator
{
    TResponse Request<TResponse>(IQuery<TResponse> query);
    TResult Send<TResult>(ICommand<TResult> query);  //This is the signature in question.
}

... навіть незважаючи на те, що не всі команди мають значуще значення для повернення.

Повторіть і заверніть

Чи правильно я фіксую, чому в цій темі плутанина? Щось мені не вистачає?

Оновлення (6/2020)

За допомогою поданих відповідей, я думаю, я розплутав плутанину. Простіше кажучи, якщо команда CQRS здатна повернути успіх / помилку, що вказує на стан завершення , тоді значення повернення має сенс. Це включає повернення нової ідентифікації рядка БД або будь-який результат, який не читає та не повертає вміст моделі домену (бізнесу).

Я думаю, що там, де виникає плутанина "команди CQRS", йдеться про визначення та роль "асинхронності". Існує велика різниця між асинхронним введенням-виводом "на основі завдань" та асинхронною архітектурою (наприклад, проміжне обладнання на основі черги). У першому "завдання" асинхронізації може і надасть результат завершення команди async. Однак команда, надіслана RabbitMQ, не отримає аналогічним чином повідомлення про завершення запиту / відповіді. Саме цей останній контекст асинхронної архітектури змушує деяких говорити "не існує такого поняття, як асинхронна команда" або "команди не повертають значень".


Приємне питання, але не зовсім зрозуміле. Команди - це просто VO, а не методи, вони нічого не повертають; ймовірно, ви маєте на увазі обробку команд; будь ласка, вкажіть рівень / рівень: презентація (тобто відпочинок), програма, рівень бізнесу (сукупності).
Константин Галбену,

Відповіді:


21

Дотримуючись поради Владика Хононова щодо боротьби зі складністю в CQRS , команда обробки команд може повертати інформацію, що стосується її результатів.

Не порушуючи принципів [CQRS], команда може безпечно повернути наступні дані:

  • Результат виконання: успіх або невдача;
  • Повідомлення про помилки або помилки перевірки, у разі несправності;
  • Номер нової версії сукупності, у разі успіху;

Ця інформація суттєво покращить взаємодію з користувачем у вашій системі, оскільки:

  • Вам не потрібно опитувати зовнішнє джерело для результату виконання команди, ви маєте це відразу. Тривіальним стає перевірка команд і повернення повідомлень про помилки.
  • Якщо ви хочете оновити відображені дані, ви можете використовувати нову версію агрегату, щоб визначити, чи відображає модель подання виконану команду чи ні. Більше не відображаються застарілі дані.

Даніель Уітакер виступає за повернення об'єкта " загального результату " з обробника команд, що містить цю інформацію.


4
А як щодо команди, яка створює новий об’єкт, чи не повинна вона повертати хоча б ідентифікатор створеного об’єкта?
Greyshack

4
@Greyshack Очевидно, тому більшість ресурсів ES / CQRS пропонують використовувати GUID для сукупних ідентифікаторів / ідентифікаторів - клієнт може генерувати ідентифікатор та включати його в команду створення, замість того, щоб залишати його для бекенда для створення ідентифікатора.
Сем Джонс,

9

Ну, обробники команд повертають значення чи ні?

Вони не повинні повертати бізнес-дані , лише метадані (щодо успіху чи невдалого виконання команди). CQRSце ОКК , прийнятий на більш високий рівень. Навіть якби ви порушили правила пуриста і щось повернули, що б ви повернули? У CQRS обробник команд є методом, application serviceякий завантажує метод, який aggregateвикликає aggregatethen, тоді як він зберігається aggregate. Метою обробника команд є модифікація aggregate. Ви не знали б, що повернути, що було б незалежно від абонента. Кожен абонент / клієнт, який обробляє команди, хотів би знати щось інше про новий стан.

Якщо виконання команди блокує (воно ж синхронне), тоді все, що вам потрібно знати, успішно виконана команда чи ні. Потім, на вищому рівні, ви запитуєте саме те, що вам потрібно знати про стан нового додатка, використовуючи модель запиту, яка найкраще відповідає вашим потребам.

Подумайте інакше, якщо ви повертаєте щось із обробника команд, ви покладаєте на нього два обов'язки: 1. модифікувати агрегований стан і 2. запитувати якусь модель читання.

Щодо перевірки команд, існує принаймні два типи перевірки команд:

  1. перевірка осудності команди, яка перевіряє наявність у команди правильних даних (тобто дійсну адресу електронної пошти); це робиться до того, як команда досягне сукупності, в обробнику команд (служба додатків) або в конструкторі команд;
  2. перевірка інваріантів домену, що виконується всередині агрегату, після того, як команда досягає агрегату (після виклику методу на агрегаті), і перевіряє, чи може агрегат змінитися на новий стан.

Однак, якщо ми піднімемося на якийсь рівень вище Presentation layer(тобто ( RESTкінцеву точку), клієнта Application layer, ми можемо повернути що завгодно, і ми не будемо порушувати правила, оскільки кінцеві точки розроблені після випадків використання, ви точно знаєте, що хочете повернення після виконання команди в кожному випадку використання.


2
"ти точно знаєш, що хочеш повернути після виконання команди, у кожному випадку використання" - ні, я не знаю. Якщо клієнт запитує послугу A, яка має CQRS, а обробник команд всередині переробляє деякі дані і викликає службу B у фоновому режимі (від імені клієнта), яка потім повертає ідентифікатор новоствореного ресурсу, як би я повернув цей ідентифікатор клієнту, якщо команда A не повертає значення?
Wirone

2
@Wirone Загалом у CQRS використовуються GUID, тому ви знаєте ідентифікатор сутності ще до того, як виконати команду. Отже, ви знаєте посвідчення особи та знаєте варіант використання => ви знаєте модель читання
Константин Галбену

2
"Вони не повинні. CQRS - це CQS, піднятий на більш високий рівень". CQRS - ідея вищого рівня, і вона не дбає про методи. Основна ідея CQRS полягає у тому, щоб уникнути змішування моделей запису та читання, але обробник команд може (ПОВИНЕН) повертати помилки та навіть створені події.
Мізантроп,

2
@Misanthrope Я згоден, будь ласка, прочитайте ще раз мою відповідь, особливо останній абзац. Однак, обробник команд (щоб бути впевненим: метод, який завантажує агрегат, викликає метод команди, а потім зберігає агрегат) не повинен нічого повертати. Якщо вам потрібні події, підпишіться на них, і обробник HTTP (а не обробник команд!) Може збирати ці події та повертати їх. Це стосується і винятків. Чорт, обробник HTTP (або все, що у вас є над обробником Command) може запитувати модель читання або щось інше і повертати якийсь стан.
Константин Галбену,

2
"Якщо вам потрібні події, то підпишіться на них" Я не бачу підстав для цього в цьому випадку. Обробник команд, загалом кажучи, просто перетворює команду в події. Отримання подій у цьому випадку є просто більш явним способом.
Мізантроп

4

CQRS та CQS схожі на мікросервіси та декомпозицію класів: основна ідея однакова ("прагнення до малих згуртованих модулів"), але вони лежать на різних семантичних рівнях.

Суть CQRS полягає у розділенні моделей запису / читання; такі деталі низького рівня, як повернення значення від конкретного методу, абсолютно не мають значення.

Зверніть увагу на наступну цитату Фаулера :

Зміна, яку запроваджує CQRS, полягає у розділенні цієї концептуальної моделі на окремі моделі для оновлення та відображення, які вона називає Command та Query відповідно, відповідно до словника CommandQuerySeparation.

Йдеться про моделі , а не про методи .

Обробник команд може повертати все, крім моделей читання: стан (успіх / невдача), згенеровані події (основна мета обробників команд, до речі: генерувати події для даної команди), помилки. Обробники команд дуже часто кидають неперевірений виняток, це приклад вихідних сигналів від обробників команд.

Більше того, автор терміна Грег Янг каже, що команди завжди синхронізуються (інакше це стає подією): https://groups.google.com/forum/#!topic/dddcqrs/xhJHVxDx2pM

Грег Янг

насправді я сказав, що асинхронна команда не існує :) це насправді ще одна подія.


2

Відповісти для @Constantin Galbenu, я стикнувся з обмеженнями.

@Misanthrope І що саме ти робиш із цими подіями?

@Constantin Galbenu, в більшості випадків вони мені не потрібні в результаті команди, звичайно. У деяких випадках - мені потрібно повідомити клієнта у відповідь на цей запит API.

Це надзвичайно корисно, коли:

  1. Вам потрібно повідомляти про помилку через події замість винятків. Зазвичай це трапляється, коли вашу модель потрібно зберегти (наприклад, підраховується кількість спроб з неправильним кодом / паролем), навіть якщо сталася помилка. Крім того, деякі хлопці взагалі не використовують винятки для ділових помилок - лише події ( http://andrzejonsoftware.blogspot.com/2014/06/custom-exceptions-or-domain-events.html ) Особливої ​​причини немає думати, що викидати бізнес-винятки з обробника команд - це нормально, але повернення подій домену - ні .
  2. Коли подія відбувається лише за певних обставин усередині вашого сукупного кореня.

І я можу навести приклад для другого випадку. Уявіть, що ми робимо службу, схожу на Tinder, у нас є команда LikeStranger. Ця команда МОЖЕ привести до StrangersWereMatched, якщо нам подобається людина, яка вже сподобалася нам раніше. Нам потрібно повідомити мобільного клієнта у відповідь, сталося збіг чи ні. Якщо ви просто хочете перевірити matchQueryService після команди, ви можете знайти там відповідність, але немає жодної гарантії, що збіг відбувся саме зараз, тому що ПЕРЕВАГА Tinder показує вже зіставлених незнайомців (можливо, в незаселених районах, можливо, невідповідність, можливо, у вас просто є 2-й пристрій тощо).

Перевірити відповідь, чи дійсно StrangersWereMatched сталося саме зараз, - це настільки просто:

$events = $this->commandBus->handle(new LikeStranger(...));

if ($events->contains(StrangersWereMatched::class)) {
  return LikeApiResponse::matched();
} else {
  return LikeApiResponse::unknown();
}

Так, ви можете ввести ідентифікатор команди, наприклад, і зробити модель зчитування Match, щоб зберегти її:

// ...

$commandId = CommandId::generate();

$events = $this->commandBus->handle(
  $commandId,
  new LikeStranger($strangerWhoLikesId, $strangerId)
);

$match = $this->matchQueryService->find($strangerWhoLikesId, $strangerId);

if ($match->isResultOfCommand($commandId)) {
  return LikeApiResponse::matched();
} else {
  return LikeApiResponse::unknown();
}

... але подумайте: чому ви вважаєте, що перший приклад з прямолінійною логікою гірший? Це все одно не порушує CQRS, я просто зробив явний явний. Це беззмінний підхід. Менше шансів потрапити на помилку (наприклад, якщо matchQueryServiceкешування / затримка [не негайно узгоджується], у вас проблема).

Так, коли факту відповідності недостатньо, і вам потрібно отримати дані для відповіді, вам доведеться скористатися сервісом запитів. Але ніщо не заважає отримувати події від обробника команд.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.