事件溯源(EventSourcing)与聚合根(Aggregates):避免重复校验业务不变量的方案咨询
这个问题我当初刚搞事件溯源的时候也纠结过——既要守住聚合根的封装性,不让业务逻辑外泄,又不想在外部写一堆丑陋的try-catch或者重复校验代码,确实有点头疼。下面分享几个业内常用的处理思路,你可以根据自己的场景选:
1. 给聚合根添加「安全尝试型」方法
既然不想在外部用try-catch,那可以把异常捕获的逻辑封装到聚合根内部,提供一个不会抛出异常、只返回操作结果的方法。比如在ProductAggregateRoot里加个tryChangePrice:
public function tryChangePrice(ChangeProductPrice $command): bool { if ($this->availability->equals(Availability::UNAVAILABLE())) { return false; } if ($this->price->equals($command->newPrice)) { return false; } $this->recordThat( new ProductPriceChanged($this->price, $command->newPrice) ); return true; }
如果需要知道操作失败的具体原因,还可以返回一个Result对象(包含成功状态、错误码/信息),比单纯的bool更严谨。外部调用时就可以直接用if判断,代码会清爽很多:
if ($aggregate->tryChangePrice(new ChangeProductPrice($productId, $state->getPrice()))) { // 价格更新成功后的逻辑(如果需要) }
2. 暴露「可执行性检查」方法(注意竞态风险)
把聚合根内部的校验逻辑提取成私有辅助方法,再对外暴露一个canChangePrice之类的检查接口。这样外部可以先判断是否能执行,再调用真正的修改方法:
// 聚合根内部 private function canChangePrice(Money $newPrice): bool { return !$this->availability->equals(Availability::UNAVAILABLE()) && !$this->price->equals($newPrice); } public function canChangePrice(ChangeProductPrice $command): bool { return $this->canChangePrice($command->newPrice); } public function changePrice(ChangeProductPrice $command): self { if (!$this->canChangePrice($command->newPrice)) { throw CannotChangePriceException::fromReason(...); } $this->recordThat( new ProductPriceChanged($this->price, $command->newPrice) ); return $this; }
外部调用:
$changePriceCmd = new ChangeProductPrice($productId, $state->getPrice()); if ($aggregate->canChangePrice($changePriceCmd)) { $aggregate->changePrice($changePriceCmd); }
⚠️ 注意:这种方式存在竞态风险——如果在canChangePrice和changePrice之间,聚合根的状态被其他线程/进程修改了,还是可能抛出异常。所以只适合单线程场景,或者能保证这段时间内聚合根状态不会变化的场景。
3. 用领域服务封装尝试逻辑
如果不想改动聚合根的现有设计(比如不想暴露try/can方法),可以把try-catch的逻辑封装到专门的领域服务里,让外部代码保持干净:
class ProductSyncService { public function syncPrice(ProductAggregateRoot $aggregate, Money $externalPrice): void { try { $aggregate->changePrice(new ChangeProductPrice($aggregate->getId(), $externalPrice)); } catch (CannotChangePriceException $ex) { // 这里可以记录日志、上报监控,或者什么都不做 // 外部调用者不用关心失败细节 } } public function syncAvailability(ProductAggregateRoot $aggregate, Availability $externalAvailability): void { try { $aggregate->changeAvailability(new ChangeProductAvailability($aggregate->getId(), $externalAvailability)); } catch (CannotChangeAvailabilityException $ex) { // 同理处理 } } }
外部调用时就非常简洁:
$syncService->syncPrice($aggregate, $state->getPrice()); $syncService->syncAvailability($aggregate, $state->getAvailability());
这种方式相当于把“丑陋”的try-catch隐藏在服务层,既保留了聚合根的封装性,又让上层代码保持优雅。
总结
- 如果想完全避免外部的try-catch,优先考虑给聚合根加
tryXXX方法; - 单线程/低并发场景下,
canXXX方法会让代码更直观; - 不想改动聚合根原有设计的话,用领域服务封装try-catch是最省心的选择。
内容的提问来源于stack exchange,提问作者ficus
相关产品推荐
相关产品推荐

