
在基于事件溯源的领域驱动设计中,聚合根(Aggregate Root)是业务不变性(Invariants)的守卫者。它封装了领域对象的行为和状态,并确保任何操作都不会破坏其内部定义的业务规则。然而,在实际应用中,尤其当需要同时更新聚合根的多个属性时,如何优雅地处理这些不变性约束,避免验证逻辑的重复或产生不必要的复杂性,是一个常见的挑战。
考虑一个 ProductAggregateRoot,其中包含 changePrice 方法,该方法在修改产品价格前会进行两项不变性检查:
public function changePrice(ChangeProductPrice $command): self
{
    // 不变性检查1: 产品不可用时不能改价
    if ($this->availability->equals(Availability::UNAVAILABLE())) {
        throw CannotChangePriceException::unavailableProduct();
    }
    // 不变性检查2: 价格未改变时无需操作
    if ($this->price->equals($command->newPrice)) {
        throw CannotChangePriceException::priceHasntChanged();
    }
    $this->recordThat(
        new ProductPriceChanged($this->price, $command->newPrice)
    );
    return $this;
}现在,假设存在一个领域服务或应用服务,需要根据外部数据源同时更新产品的价格和可用性。如果简单地为每个属性的更新方法(如 changePrice 和 changeAvailability)分别包裹 try-catch 块,或者在服务层重复聚合根内部的 CanChangePrice() 类似检查,都会导致代码的冗余、耦合度增加,并可能掩盖真正的业务意图。这种做法不仅显得笨拙,也违背了聚合根作为不变性边界的初衷。
解决上述问题的核心思路之一是引入“复合命令”(Composite Command)。当业务操作涉及聚合根多个属性的协同更新,并且这些更新共同构成一个完整的业务意图时,应该将它们封装成一个单一的、更具表达力的命令。
例如,如果需要根据外部数据源同步产品的价格和可用性,可以定义一个 SyncProductData 或 UpdateProductFromExternalSource 这样的命令,并让聚合根处理这个命令。
复合命令的优势:
示例:处理复合命令
首先,定义一个复合命令:
final class SyncProductData
{
    public readonly ProductId $productId;
    public readonly Price $newPrice;
    public readonly Availability $newAvailability;
    public function __construct(ProductId $productId, Price $newPrice, Availability $newAvailability)
    {
        $this->productId = $productId;
        $this->newPrice = $newPrice;
        $this->newAvailability = $newAvailability;
    }
}然后,在 ProductAggregateRoot 中添加一个处理此命令的方法:
public function syncData(SyncProductData $command): self
{
    // 在这里进行整体的、上下文感知的不变性检查
    // 检查逻辑会考虑新的价格和可用性组合
    // 示例:如果新状态是可用,并且价格有变化,则允许更新
    if ($command->newAvailability->equals(Availability::AVAILABLE()) && !$this->price->equals($command->newPrice)) {
        // 记录价格变更事件
        $this->recordThat(new ProductPriceChanged($this->price, $command->newPrice));
    }
    // 示例:如果可用性有变化,则记录可用性变更事件
    if (!$this->availability->equals($command->newAvailability)) {
        $this->recordThat(new ProductAvailabilityChanged($this->availability, $command->newAvailability));
    }
    // 注意:这里的逻辑需要根据具体的业务规则进行调整
    // 比如,如果产品从不可用变为可用,并且价格也同时更新,
    // 那么之前的“不可用时不能改价”的规则可能就需要被重新评估,
    // 或者在这个复合操作中被允许。
    return $this;
}通过这种方式,外部服务只需向聚合根发送一个 SyncProductData 命令,聚合根将负责协调内部状态的更新和所有相关的不变性检查。
在原始的 changePrice 方法中,如果 newPrice 与 this->price 相同,会抛出 CannotChangePriceException::priceHasntChanged() 异常。这种处理方式值得商榷。
重新评估的理由:
建议的处理方式:
当检测到命令不会导致状态发生实际改变时,聚合根可以直接返回自身($this),而不抛出异常。这意味着聚合根“默许”了该操作,因为目标状态已经达成。
public function changePrice(ChangeProductPrice $command): self
{
    if ($this->availability->equals(Availability::UNAVAILABLE())) {
        throw CannotChangePriceException::unavailableProduct();
    }
    // 优化:如果价格未改变,直接返回,不抛出异常
    if ($this->price->equals($command->newPrice)) {
        return $this; // 价格已是目标值,无需操作
    }
    $this->recordThat(
        new ProductPriceChanged($this->price, $command->newPrice)
    );
    return $this;
}这种处理方式更符合“命令是表达意图”的原则,并简化了外部服务与聚合根的交互。只有当命令的执行会破坏核心业务规则(如“产品不可用时不能改价”)时,才应该抛出异常。
在事件溯源和聚合根的设计中,优雅地处理不变性约束是构建健壮领域模型的关键。通过引入复合命令,我们能够更好地捕捉复杂的业务意图,将多属性更新的协调和不变性检查集中在聚合根内部,从而避免了外部服务的冗余逻辑和 try-catch 滥用。同时,重新审视“无实际改变”的异常处理策略,让聚合根在目标状态已达成时直接返回,可以提高命令的幂等性,并简化调用方的逻辑。遵循这些策略,有助于构建出结构清晰、逻辑严谨、易于维护的领域模型。
以上就是事件溯源与聚合:不变性约束的优雅处理策略的详细内容,更多请关注php中文网其它相关文章!
                        
                        每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
                
                                
                                
                                
                                
                                
                                Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号