Проверка / применение инвариантов в приложении CQRS - PullRequest
0 голосов
/ 09 июня 2018

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

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

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

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

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

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

  • Совокупный корень рекламы
  • Элемент AR
  • 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.Мне не очень комфортно с этой опцией, так как я понимаю, что пытаюсь добиться согласованности транзакций, когда она мне действительно не нужна (хотя мне нужно минимизировать риски для команд с отсутствующим количеством или неактивным членом, если это произойдет, это не так уж сложно, и после этого я предпринимаю корректирующее действие, поэтому я вполне согласен с возможной последовательностью).

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

Ответы [ 3 ]

0 голосов
/ 11 июня 2018

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

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

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

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

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

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

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

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

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

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

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

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, используемый для идентификации процесса покупки рекламы. Это также можно рассматривать как тип идентификатора корреляции. Вы можете сбросить его.

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

Если команда BuyAd завершается неудачно, то компенсация не требуется, но если команда BuyerBuysAd завершается неудачей, то компенсация выполняется путем отправкиКоманда CancelAdBuy для Ad Aggregate.

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

0 голосов
/ 11 июня 2018

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

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

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

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

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

0 голосов
/ 09 июня 2018

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

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

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

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

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

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

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

...