php - 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
- 库存被接收到一个位置
- 库存从一个位置 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) 它可能会很快变得复杂。这只是众多规则之一。
解决方案
我认为这里的主要问题是您跳过了一个重要的事实,event-sourcing
即events
存储。
在您的投影中,看起来您正在存储实体。预测基本上Listeners
是对发生的事情做出反应。
通常,您需要应用以下场景
- 从事件列表中组成您的聚合根(如果还没有事件,则为空)
- 在你的 AR 中应用业务逻辑(这应该是业务不变量的真实来源)
- 记录一个事件,如
StockMadeAvailable
- 将您的事件存储在
Event Store
-> 中,这可能是MySQL
- 您可以使用您的预测来监听您的事件或外部事件并更新您的读取模型,它们基本上是为提供高性能查询而开发的数据库表
此外,Spatie 提供了一个基础设施,Event Sourcing
但您自己的聚合/实体/业务不变量的组合仍然取决于您的业务需求
更新#1
关于这一点 - a) 它不会扩展。根据使用的位置数量,该数组可能会增长到数千个元素。
- 我宁愿与业务人员/领域专家交谈,以确定地点的峰值和平均数量
- 使用一组实体和值对象可能会给您带来灵活性
array $locations
,而不是您可以使用散列映射或适合您需要的不同数据结构。
b) 它可能会很快变得复杂。这只是众多规则之一。- 通常,每个规则都有不同的功能 - 如果太复杂,为什么不使用 Domain 与 DomainRules
相同的想法Exceptions
这只是可能有帮助的想法。
推荐阅读
- c# - 是否有将 DataRow 映射到类对象的方法
- c++ - 如何更改 QFileDialog 窗口的图标
- mongodb - MongoDB Aggregation SUM Array of Arrays by object key
- javascript - Javascript 返回 System.InvalidOperationException: Unexpected new line Error
- xslt-1.0 - XSL 通过拆分元素的值来替换元素
- java - 为什么 ”;” 和 ”\\;” 找到一样的?
- html - 如何制作等高的柱子
- python - 如何找到字符串的第一个数字
- javascript - 使用 javascript 的会话
- sql-server - ionic 2+ SqlServer 上的导入未定义(cordova-plugin-sqlserver)