【问题标题】:Laravel Event Sourcing (Spatie) - How to deal with more complex business rulesLaravel Event Sourcing (Spatie) - 如何处理更复杂的业务规则
【发布时间】:2021-09-20 00:06:29
【问题描述】:

我目前已开始使用 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;
}

更复杂 - 附加属性

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

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

  • 库存被接收到一个位置
  • 库存从一个位置 A 移动到位置 B
  • 库存从位置 A 分配给订单 1
  • 从位置 A 为订单 1 挑选库存
  • 库存被包装到订单 1 的装运包装 Z 中

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

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) 它可能会很快变得复杂。这只是众多规则之一。

【问题讨论】:

    标签: php laravel event-sourcing


    【解决方案1】:

    我认为这里的主要问题是您忽略了event-sourcing 中的一个重要事实,即events 存储。

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

    一般情况下,您需要应用以下场景

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

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

    更新 #1

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

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

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

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

    【讨论】:

    • 谢谢。我试图在这里使用事件存储作为事实来源。我正在生成事件,可以回复它们以构建预测。这一切似乎都很好。我现在的问题是我如何在事件中存储相当复杂的模型和逻辑,然后在聚合根中查询它们。我将在主要问题中添加一个示例
    • @AdamLambert 我明白了,“感觉就像我必须在我的投影仪中复制代码才能检查事件中的数据。”这部分有点令人不安。我更新了我的回复:)
    • 感谢您的回复和编辑。我已经投票给你,但我不确定我是否真的在问正确的问题以获得我需要的答案。我在这里再次尝试了一个更好的例子! stackoverflow.com/questions/58455116/…
    猜你喜欢
    • 1970-01-01
    • 2017-11-08
    • 1970-01-01
    • 2022-01-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-22
    • 2010-12-04
    相关资源
    最近更新 更多