Laravel Event Sourcing (Spatie) — использование прогнозов в бизнес-правилах

Я знаю, что общая концепция источника событий заключается в том, что состояние приложения должно иметь возможность воспроизводиться из потока событий.

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

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

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

Пользовательская таблица/модель в настоящее время не является источником событий.

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

Итак, если мой текущий пример не работает, мои вопросы:

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

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

public function subtractMoney(int $amount)
{
    if ($this->accountOwnerIsBlacklisted()){
        $this->recordThat(new UserActionBlocked());

        throw CouldNotSubtractMoney::ownerBlocked();
    }

    if (!$this->hasSufficientFundsToSubtractAmount($amount)) {
        $this->recordThat(new AccountLimitHit());

        if ($this->needsMoreMoney()) {
            $this->recordThat(new MoreMoneyNeeded());
        }

        $this->persist();

        throw CouldNotSubtractMoney::notEnoughFunds($amount);
    }

    $this->recordThat(new MoneySubtracted($amount));
}

private function accountOwnerIsBlacklisted(): bool
{
    return $this->accountRepositry()->ownerUser()->isBlackListed();
}

person Adam Lambert    schedule 18.10.2019    source источник


Ответы (1)


Поскольку вы в основном работаете с DDD (не упоминая об этом), ответ может заключаться в определениях. В DDD вы должны определить границы каждого совокупного корня. Каждый совокупный корень не должен хранить никаких зависимостей от других совокупных корней (пакет Spatie даже не поддерживает это). Оно должно состоять только из событий, которые затем становятся единственным источником истины.

Учитывая ваш пример, кажется, что блокировка пользователя связана не с негативными событиями на его аккаунте, а с чем-то, что произошло в отношении его пользователя (владельца аккаунта). Ключевое слово здесь, кажется, "владелец". Если вы хотите сохранить тот факт, что произошло действие пользователя по попытке снять деньги, вы все равно можете применить событие, но в этом случае причина будет исходить от другого агрегата «пользователь». Неважно, является ли сам пользователь источником события, но у сущности пользователя есть метод, чтобы проверить, заблокирован ли пользователь, и, следовательно, в вашей системе есть бизнес-правило, согласно которому ему не разрешено снимать средства со счета. Если вы не можете смоделировать эти две модели вместе, я бы посоветовал вам разработать доменную службу, которая может обрабатывать эту команду. Постарайтесь сохранить их как часть вашей модели, чтобы не сделать модель предметной области анемичной, если это возможно.

<?php 
class AccountWithdrawalService
{
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function withdraw($userId, $accountId, $amount)
    {
        $user = $this->userRepository->find($userId);

        // You might inject AccountAggregateRoot too.
        $account = AccountAggregateRoot::retrieve($accountId);

        if(!$user->isBlackListed())
        {
            $account->subtractMoney($amount);
        }
        else
        {
            // Here we record the unhappy road :-( 
            $account->moneySubtractionBlocked($amount);
        }

        $account->persist();
    }
}

PS: Еще одна возможность — внедрить ваш userRepository в фактический метод, обрабатывающий снятие средств, если userRepository не является полной зависимостью от AccountAggregateRoot. Это, я думаю, широко обсуждается.

person nicolaib    schedule 18.11.2019
comment
Мне нравится этот ответ. Класс службы также может обрабатывать большую часть бизнес-логики без необходимости раздувания корня Aggregate. - person anabeto93; 24.04.2021