Проверка / применение инвариантов в приложении CQRS

Я искал SO и другие форумы / списки рассылки в поиске ответов по этой теме, и, хотя я знаю, что ответ в значительной степени зависит от самого домена и того, что является приемлемым с точки зрения возможной согласованности, я все еще борюсь за найти хорошее решение.

Проблема связана с тем, где проверять бизнес-правила, соответствующие домену.

Мой домен является онлайн-рынком. Участник (с ролью Продавец) может опубликовать объявление о продаже предмета. Продавец может указать минимальное и максимальное количество товаров, которые можно приобрести за один заказ, а также цену товара.

Покупатель может купить товар за определенную рекламу. Необходимо соблюдать следующие правила:

  • Они могут указать количество товаров, которые они хотели бы купить, и это должно быть от минимума до максимума, разрешенного рекламой.
  • Они должны быть активными (поскольку участники могут быть забанены).
  • Объявление должно быть активным (реклама может быть приостановлена).

My Market BC — это тот, который занимается рекламой и покупкой сделок. Я разработал это следующим образом:

  • Ad Aggregate root
  • Член АР
  • BuyTransaction AR

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

$buyer->buy($adId, $quantity);

Это будет вызвано командой BuyItems

$buyCommand = new BuyItems($adId, $qty);

На член совокупности.

Из вариантов, которые я собираю, у меня есть:

  1. Проверка вне домена, во внешнем слое — это значит, что я бы проверил команду перед отправкой в ​​домен. Это подразумевает некоторую утечку логики за пределы домена, но я получу объявление из модели чтения, проверим ограничение (между min и max, объявление активно, пользователь активен), а затем отправлю команду.
    В этом случае я также выполняю валидацию на стороне домена в форме диспетчера процессов, который выдаст компенсирующее действие или, по крайней мере, предупредит, если возникнет несоответствие.

  2. Определите интерфейс службы в домене и внедрите службу, которая получает данные из модели чтения, затем проверьте в обработчике команд, вызвав службу. Если данные недействительны, выведите исключение.
    Проверка домена также должна происходить и здесь, потому что модель чтения может быть непоследовательной (опять же с использованием диспетчера процессов).

  3. Загрузите агрегированные корни Ad и Member в обработчике BuyItem и передайте его $ Buy-> Buy ($ ad, $ member, $ qty); затем в методе buy () в AR проверьте, что qty находится между min и max. Мне не очень комфортно с этой опцией, так как я понимаю, что пытаюсь добиться согласованности транзакций, когда она мне действительно не нужна (хотя мне нужно минимизировать риски для команд с отсутствующим количеством или неактивным членом , если это произойдет, это не так уж и сложно, и после этого я предпринимаю корректирующие действия, поэтому я вполне согласен с возможной последовательностью).

Может кто-нибудь указать мне, что лучший вариант дается в этом сценарии?

1

Решение

У вас есть бизнес-процесс, который охватывает несколько агрегатов, это точно. Для этого у вас есть два варианта:

  1. Измените границу агрегатов, объединив несколько типов агрегатов в один. Код проще, компенсации выполняются базой данных автоматически при откате. Масштабируемость не так велика.

  2. Используйте сагу для моделирования всего процесса. Вам нужно отправлять компенсирующие команды для каждого сбоя. Это вариант, о котором я напишу в оставшейся части ответа.

В основном вам приходится выбирать между одной большой (глобальной) транзакцией и несколькими меньшими (локальными) транзакциями.

Сага должна содержать только координационную логику, она не должна обеспечивать соблюдение бизнес-правил самостоятельно. Подсказка о том, как его смоделировать, заключается в следующем: когда вы добавляете новое бизнес-правило, касающееся процесса покупки рекламы, сага не должна изменяться.

Бизнес-правила (инварианты) должны проверяться каждым Агрегатом, которому принадлежат данные, необходимые для проверки. Например:

Правило 1: они могут указать количество предметов, которые они хотели бы купить, и это должно быть от минимума до максимума, разрешенного рекламой. — Совокупность объявлений

Правило 2: они должны быть активными (поскольку участники могут быть забанены — Агрегат покупателя

Правило 3: реклама должна быть активной (реклама может быть приостановлена) — Агрегат рекламы

Правила 1 и 3 проверяются Ad::buyedBy($buyerId, $quantity) и правило 2 проверяется Buyer::buyAd($buyerId, $quantity), Сага просто склеит эти вызовы методов. Как это сделать, это зависит от ваших низкоуровневых требований архитектуры и устойчивости.

Предположим, что вы будете использовать стиль, продвигаемый cqrs.nu где Агрегаты обрабатывают Команды (у них есть такие методы, как handleXXX(XXX $command)), лайк Я бы сделал, тогда ваши агрегаты и ваша сага будут выглядеть так:

class Ad
{
function handleBuyAd(BuyAd $command)
{
if (!$this->active) {
throw new \Exception("Ad not active");
}
if ($command->quantity < $this->minimum || $command->quantity > $this->maximum) {
throw new \Exception("Too litle or too many");
}

yield new AdWasBuyed($this->id, $command->buyerId, $command->quantity);
}

function handleCancelAdBuy(CancelAdBuy $command)
{
yield new AdBuyinWasCancelled($this->id, $command->buyerId, $command->quantity);
}
}

class Buyer
{
function handleBuyerBuysAd(BuyerBuysAd $command)
{
if ($this->banned) {
throw new \Exception("Buyer is banned");
}

yield new BuyerBuyedAd($command->transactionId, $this->id, $command->buyerId, $command->quantity);
}
}

class BuyAdSaga
{
/** @var CommandDispather  */
private $commandDispatcher; //injected

function start($transactionId, $adId, $buyerId, $quantity)
{
$this->commandDispatcher->dispatchCommand(new BuyAd($transactionId, $adId, $buyerId, $quantity));
}

function processAdWasBuyed(AdWasBuyed $event) //"process" means only once
{
try {
$this->commandDispatcher->dispatchCommand(new BuyerBuysAd($event->transactionId, $event->adId, $event->buyerId, $event->quantity));
} catch (\Exception $exception) {
// this is a compensating command
$this->commandDispatcher->dispatchCommand(new CancelAdBuy($event->transactionId, $event->adId, $event->buyerId, $event->quantity));
}
}
}

Команды содержат $transationId используется для идентификации процесса покупки рекламы. Это также можно рассматривать как тип Id корреляции. Вы можете выбросить это.

Сага начинается start метод. Вы также можете сбросить его и считать, что Сага началась, отправив первую команду в Агрегат объявлений. Я сделал это, чтобы быть более понятным, как этот процесс начинается.

Если BuyAd команда не выполняется, тогда компенсация не требуется, но если BuyerBuysAd команда не выполняется, тогда компенсация выполняется путем отправки команды CancelAdBuy к рекламному агрегату.

Обратите внимание, что эта сага реагирует только на события, отправляя команды и ничего более. Он не применяет никаких бизнес-инвариантов, он просто координирует весь процесс.

1

Другие решения

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

Обычно есть две разные проверки. Первый — это проверка сообщения; это ваш случай, сообщение о покупке. Имеются ли в нем все необходимые данные, есть ли данные в правильной форме и т. Д. Этот шаг проверки смотрит на сообщение в изоляции, во многом так, как если бы вы проверяли XML-документ.

Предполагая, что это командное сообщение, мы теперь передаем его модели домена для обработки. Доменная модель владеет бизнес-логикой как модель изменится в ответ на сообщение.

Таким образом, модель домена знает, является ли объявление активным, имеет ли этот конкретный член хорошую репутацию, является ли количество элементов разумным. Таким образом, он должен решить, как это изменится. Возможно это не изменится вообще — неявно отбрасывая сообщение. Возможно, это изменится, добавив сообщение в явный список отклоненных сообщений.

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

Иногда это намек на то, что границы ваших агрегатов не совсем верны; в других это означает, что вы не думаете о чтении правильно.

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

1

Они могут указать количество предметов, которые они хотели бы купить,
который должен быть между минимальным и максимальным разрешенным объявлением.

Они должны быть активными (поскольку участники могут быть забанены).

Объявление должно быть активным (реклама может быть приостановлена).

Похоже, что № 1 и № 3 можно решить, сделав Ad породить новый BuyTransactionпримерно так же, как Вот.

Что касается # 2, я никогда не видел, чтобы системы обеспечивали действительность пользователя посредством немедленной согласованности на уровне домена (т. Е. Проверка того, что текущий пользователь активен в той же транзакции, что и та, в которой участвует Ad совокупный корень). Я бы делегировал это на уровень контроля доступа.

0
По вопросам рекламы [email protected]