首页 > 解决方案 > Laravel Event Sourcing (Spatie) - 如何处理更复杂的业务规则

问题描述

我目前已经开始使用 spaties 的包spatie/laravel-event-sourcing 进入事件溯源领域

在遵循基本设置和指南以及阅读/观看许多其他事件溯源指南之后,我有了基本的了解,但我很难理解应该如何实施更复杂的业务规则。

简单规则 - 银行余额示例

下面的例子是直截了当的,完全有意义。您从每个事件的余额中减去钱。如果您低于阈值,则在被允许降低之前会引发异常。

public function subtractMoney(int $amount)
{
    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));
}

protected function applyMoneySubtracted(MoneySubtracted $event)
{
    $this->balance -= $event->amount;

    $this->accountLimitHitInARow = 0;
}

更复杂 - 附加属性

在上面的例子中,我们只有一个属性(数量)。

在我的用例中,我的聚合根是产品库存。我希望聚合能够捕获跨不同存储位置的所有库存移动。IE

进行预测实际上非常简单。例如,我的库存投影仪显示每个位置的每种产品以及接收、分配的库存量等。

public function onStockReceived(StockReceived $event, string $aggregateUuid)
{
    $inventory = Inventory::firstOrNew([
        'product_id' => $aggregateUuid,
        'location_id' => $event->locationId
    ]);

    $inventory->received += $event->amount;

    $inventory->save();
}

在尝试确定业务规则时,我的问题又回到了聚合根中。感觉就像我必须在我的投影仪中复制代码才能检查事件中的数据。

在下面的示例中,我想确保我不会提供比收到的更多的库存。即不要低于0。这开始感觉非常复杂并且重复计数已经存储在我的投影仪中。

public function makeStockAvailable(int $amount, $locationId)
{
    if($this->hasInsufficientStockToMakeAvailable($amount, $locationId)){
        throw CouldNotMakeStockAvailable::insufficientStock($amount, $this->locations[$locationId]['received']);
    }

    $this->recordThat(new StockMadeAvailable($amount, $locationId));

    return $this;
}

public function applyStockMadeAvailable(StockMadeAvailable $event)
{
    $this->stockMadeAvailableInLocation($event->amount, $event->locationId);

    $this->availableStockTotal($event->amount);
}

private function stockMadeAvailableInLocation($amount, $locationId)
{
    $this->locations[$locationId]['received'] = $this->locations[$locationId]['received'] ?? 0;

    $this->locations[$locationId]['received'] -= $amount;

    $this->locations[$locationId]['available'] = $this->locations[$locationId]['available'] ?? 0;

    $this->locations[$locationId]['available'] += $amount;
}

private function availableStockTotal($amount)
{
    $this->received -= $amount;

    $this->available += $amount;
}

private function hasInsufficientStockToMakeAvailable($amount, $locationId): bool
{
    if(isset($this->locations[$locationId]['received'])){
        return $this->locations[$locationId]['received'] - $amount < 0;
    }

    return false;
}

我认为在我的聚合根(查找投影仪)中使用 Eloquent 是不行的,因为这会导致大量的数据库查询,并且我不确定 AR 是否应该依赖于投影进行决策?

此外,无法在我的投影仪中添加任何业务规则,因为这是在活动已经获得批准之后。

我真的很喜欢 spaties 包,因为它使事件溯源的基础知识变得容易,但感觉有一个很大的飞跃使它可以与更复杂的解决方案一起使用。

要解决的示例业务逻辑

public function moveAvailableStock(int $amount, $locationIdFrom, $locationIdTo)
{
    if(($available = $this->locations[$locationIdFrom]['available'] ?? 0) < $amount){
        throw CouldNotMoveStock::insufficientStock($amount, $available, $locationIdFrom);
    }

    $this->recordThat(new StockMoved($amount, $locationIdFrom, $locationIdTo));

    return $this;
}

当我重播事件时,我需要通过其状态(已接收、可用、已分配)以及它们的存储位置来存储每个库存移动。我目前正在使用存储到私有变量 ($locations) 的数组来执行此操作,并且它可以工作,但它确实感觉像:

a) 它不会扩展。根据使用的位置数量,该数组可能会增长到数千个元素。

b) 它可能会很快变得复杂。这只是众多规则之一。

标签: phplaravelevent-sourcing

解决方案


我认为这里的主要问题是您跳过了一个重要的事实,event-sourcingevents存储。

在您的投影中,看起来您正在存储实体。预测基本上Listeners是对发生的事情做出反应。

通常,您需要应用以下场景

  1. 从事件列表中组成您的聚合根(如果还没有事件,则为空)
  2. 在你的 AR 中应用业务逻辑(这应该是业务不变量的真实来源)
  3. 记录一个事件,如StockMadeAvailable
  4. 将您的事件存储在Event Store-> 中,这可能是MySQL
  5. 您可以使用您的预测来监听您的事件或外部事件并更新您的读取模型,它们基本上是为提供高性能查询而开发的数据库表

此外,Spatie 提供了一个基础设施,Event Sourcing但您自己的聚合/实体/业务不变量的组合仍然取决于您的业务需求

更新#1

关于这一点 - a) 它不会扩展。根据使用的位置数量,该数组可能会增长到数千个元素。

  • 我宁愿与业务人员/领域专家交谈,以确定地点的峰值和平均数量
  • 使用一组实体和值对象可能会给您带来灵活性array $locations,而不是您可以使用散列映射或适合您需要的不同数据结构。

b) 它可能会很快变得复杂。这只是众多规则之一。- 通常,每个规则都有不同的功能 - 如果太复杂,为什么不使用 Domain 与 DomainRules相同的想法Exceptions

这只是可能有帮助的想法。


推荐阅读