У вас есть бизнес-процесс, который охватывает несколько агрегатов, это точно.Для этого у вас есть два варианта:
Изменить границу агрегатов, объединив несколько типов агрегатов в один.Код проще, компенсации выполняются базой данных автоматически при откате.Масштабируемость не так велика.
Используйте сагу для моделирования всего процесса.Вам необходимо отправлять компенсирующие команды для каждого сбоя.Это вариант, о котором я напишу в оставшейся части ответа.
В основном вам приходится выбирать между одной большой (глобальной) транзакцией и несколькими меньшими (локальными) транзакциями.
Сага должна содержать только координационную логику, она не должна самостоятельно применять бизнес-правила.Подсказка о том, как его смоделировать, заключается в следующем: при добавлении нового бизнес-правила, касающегося процесса покупки рекламы, Сага не должна изменяться.
Бизнес-правила (инварианты) должны проверяться каждым агрегатом, которыйвладеет данными, необходимыми для проверки.Например:
Правило 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.
Обратите внимание, что эта сага реагирует только на события, отправляя команды и ничего более.Он не применяет никаких бизнес-инвариантов, он просто координирует весь процесс.