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

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

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

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

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

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

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

  • Совокупный корень объявлений
  • Член АР
  • КупитьТранзакция AR

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

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

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

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

В совокупности участников.

Из вариантов я понимаю, что у меня есть:

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

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

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

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


person Konel Sum    schedule 08.06.2018    source источник


Ответы (3)


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

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

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

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

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

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

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

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

Правило 3. Объявление должно быть активным (объявления могут быть приостановлены) — ​​The Ad Aggregate

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

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

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

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

person Constantin Galbenu    schedule 11.06.2018
comment
Масштабируемость не так велика. Вот почему я до сих пор избегал этого решения. Тем не менее, Уди Дахан продвигал метод, который также может быть возможен в этом случае, здесь: /" rel="nofollow noreferrer">udidahan.com/2008/02/29/. В BuyTransaction::Initiate($ad, $buyer, $seller); например, почему бы не пройти в Ad AR и проверить ограничения там? В конце концов, работа BuyTransaction (в бумажном бизнесе это было бы в любом случае) состоит в том, чтобы подтвердить, что Сделка соответствует ограничениям... Таким образом, проверка может произойти довольно рано. я что-то пропустил? - person Konel Sum; 11.06.2018
comment
Агрегаты @KonelSum не должны зависеть от данных, которыми они не владеют. Кроме того, в CQRS по определению не запрашивается модель записи. - person Constantin Galbenu; 11.06.2018
comment
@KonelSum Если вы будете следовать этим двум правилам, вы проверите инварианты в правильном агрегате. В противном случае вы пойдете к анемичной модели домена, имея что-то вроде этого BuyTransaction::Initiate($ad, $buyer, $seller), которое крадет логику из двух агрегатов. - person Constantin Galbenu; 11.06.2018
comment
@KonelSum, сделав это, вы пожертвуете одновременным доступом к удобству / сплоченности. На всем протяжении BuyTransaction Ad AR будет заблокирован для доступа к другим транзакциям. Это может быть вполне допустимым вариантом, если для Ad не так много состязаний, но может вызвать проблемы с блокировкой в ​​контексте интенсивных транзакций. - person guillaume31; 11.06.2018
comment
@constantin процесс означает только один раз, вы добавили это на случай, если мы переиграем? - person Konel Sum; 11.06.2018
comment
@KonelSum точно. - person Constantin Galbenu; 11.06.2018

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

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

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

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

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

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

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

person VoiceOfUnreason    schedule 09.06.2018
comment
Спасибо, это познавательно. Как правило, сообщение перенаправляется на совокупность, которая (возможно) изменится: в этом случае совокупность создается как минимум в моем сознании - создается новый Buy AR. и возможность доступа к другим данным модели, необходимым для обработки сообщения, передается агрегату в качестве аргументов. Подпадают ли варианты 2 и 3, которые я описал, под этот вариант? А иногда это намек на то, что границы ваших агрегатов не совсем правильные. Думал и об этом — намеки на то, как я мог думать об этом по-другому? - person Konel Sum; 09.06.2018

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

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

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

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

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

person guillaume31    schedule 11.06.2018
comment
Справедливое замечание по поводу № 2 - я попытался немного упростить вопрос. Однако система должна быть достаточно детализированной, чтобы позволить, например, полностью ограничить транзакции участников или ниже произвольно определенного уровня (т. е. никаких транзакций на сумму более 10 000 долларов США). На уровне AC можно было просто определить, может ли пользователь войти в систему или нет. - person Konel Sum; 11.06.2018
comment
Должен ли принудительный ввод ограничений быть немедленно совместимым с работой домена или он может допускать небольшую задержку/устаревание? - person guillaume31; 11.06.2018
comment
В большинстве случаев он может допустить некоторую задержку. - person Konel Sum; 11.06.2018
comment
Затем у вас есть возможность проверить ограничения вне основной транзакции или с сагой, как объяснил Константин. - person guillaume31; 11.06.2018