You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD聚合内实体变更实现疑问与复杂业务规则处理咨询

聚合内实体变更的困惑与解决方案

我的问题背景

在阅读了《DDD - Modifications of child objects within aggregate》和《Update an entity inside an aggregate》之后,我仍对聚合内实体变更的实现感到困惑。按照我的理解,聚合根代表整个聚合,会将变更指令向下委派,但这个委派过程我遇到了具体问题。

以修改指定订单项数量为例:我调用聚合根Order的方法,要求修改某一本地ID对应的订单项数量,在满足所有业务规则后创建事件并在聚合根上应用——目前所有事件均由聚合根创建并应用,我认为这符合最佳实践。

我的实现代码如下:

class Order extends AggregateRoot {
    private $orderLines = [];
    public function changeOrderLineQuantity(string $id, int $quantity) {
        if ($quantity < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }
        $this->applyChange(new OrderLineQuantityChangedEvent(
            $id, $quantity
        ));
    }
    private function onOrderLineQuantityChangedEvent(OrderLineQuantityChangedEvent $event) {
        $orderLine = $this->orderLines[$event->getId()];
        $orderLine->changeQuantity($event->getQuantity());
    }
}
class OrderLine extends Entity {
    private $quantity = 0;
    public function changeQuantity(int $quantity) {
        if ($quantity < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }
        $this->quantity = $quantity;
    }
}

但该实现存在问题:数量校验的业务规则同时存在于Order和OrderLine两个类中。若移除Order中的校验,会生成无法应用的事件;我尝试通过在OrderLine中添加canChangeQuantity方法来避免重复逻辑,但随着模型复杂度提升,该做法会越来越繁琐。

我现在有两个核心疑问:
(1) 如何处理由聚合根发起的聚合内深层实体变更?
(2) 当业务规则复杂度增加(如周一最大数量为13,产品X最大数量为3等),为聚合根的命令/方法配备领域服务进行规则校验是否为最佳实践?


针对问题的解答

问题1:处理聚合根发起的深层实体变更

你的核心痛点是重复的业务规则校验,本质是把「命令请求校验」和「实体状态变更校验」混在了一起。我们可以通过明确职责边界来解决:

  • 聚合根的核心职责是守卫聚合的整体一致性与边界:比如确认目标实体属于当前聚合、校验涉及多个实体的跨规则,而不是重复实体自身的状态校验。
  • 实体(如OrderLine)的职责是维护自身状态的合法性:它的变更方法只需要校验自身状态变更的逻辑,不需要关心聚合层面的规则。

调整后的代码示例:

class Order extends AggregateRoot {
    private $orderLines = [];

    public function changeOrderLineQuantity(string $id, int $quantity) {
        // 聚合根先做边界校验:确认订单项存在
        if (!isset($this->orderLines[$id])) {
            throw new \Exception("Order line not found.");
        }
        $orderLine = $this->orderLines[$id];
        
        try {
            // 直接委托实体执行变更,实体自己处理状态校验
            $orderLine->changeQuantity($quantity);
            // 变更合法后,由聚合根统一生成事件(保证事件的权威性)
            $this->applyChange(new OrderLineQuantityChangedEvent($id, $quantity));
        } catch (\Exception $e) {
            throw new \Exception("Failed to update order line quantity: " . $e->getMessage());
        }
    }

    private function onOrderLineQuantityChangedEvent(OrderLineQuantityChangedEvent $event) {
        // 事件回放时直接更新状态:因为事件是合法变更后生成的,无需重复校验
        $this->orderLines[$event->getId()]->setQuantity($event->getQuantity());
    }
}

class OrderLine extends Entity {
    private $quantity = 0;

    // 业务变更专用方法,包含自身状态校验
    public function changeQuantity(int $quantity) {
        if ($quantity < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }
        $this->quantity = $quantity;
    }

    // 仅用于事件回放的方法,无校验逻辑
    public function setQuantity(int $quantity) {
        $this->quantity = $quantity;
    }
}

这样调整后:

  • 聚合根只做边界和聚合层面的校验,实体负责自身状态校验,避免了规则重复。
  • 事件回放时直接更新状态,因为事件是经过合法校验后生成的,无需再次校验。

如果你的场景需要提前确认变更可行性,也可以在实体中添加canChangeQuantity方法,但要注意把校验逻辑抽成私有方法,让canChangeQuantity和changeQuantity共用同一套规则,避免出现“校验通过但变更失败”的矛盾。

问题2:复杂业务规则的校验与领域服务的使用

当业务规则变得复杂时,要先区分规则的类型,再选择合适的处理方式:

1. 聚合内部规则:由聚合根处理

如果规则依赖当前聚合内的其他实体状态(比如“订单中同一产品的总数量不能超过10”),直接在聚合根的命令方法中校验即可:

public function changeOrderLineQuantity(string $id, int $quantity) {
    $orderLine = $this->orderLines[$id];
    $productId = $orderLine->getProductId();
    
    // 计算当前订单中该产品的总数量
    $totalQuantity = array_reduce($this->orderLines, function($sum, $line) use ($productId) {
        return $line->getProductId() === $productId ? $sum + $line->getQuantity() : $sum;
    }, 0);
    
    // 校验聚合内部规则:同一产品总数量不超过10
    if ($totalQuantity + ($quantity - $orderLine->getQuantity()) > 10) {
        throw new \Exception("Total quantity for product {$productId} cannot exceed 10.");
    }
    
    // 执行变更...
}
2. 全局/跨聚合规则:用领域服务处理

如果规则不依赖当前聚合状态,或者需要访问其他聚合/领域数据(比如“周一单个订单项数量不超过13”、“产品X最大订购数量为3”),就适合用领域服务来封装这些规则。

领域服务的核心原则是:只做规则校验,不修改聚合状态,保持聚合的封装性。示例代码:

class OrderDomainService {
    private $productRepository;
    private $dateProvider; // 注入日期提供者,方便测试

    public function __construct(ProductRepository $productRepository, DateProvider $dateProvider) {
        $this->productRepository = $productRepository;
        $this->dateProvider = $dateProvider;
    }

    public function validateOrderLineQuantityChange(Order $order, string $lineId, int $quantity) {
        $orderLine = $order->getOrderLine($lineId);
        $product = $this->productRepository->findById($orderLine->getProductId());
        
        // 规则1:产品X的最大数量为3
        if ($product->getId() === "productX" && $quantity > 3) {
            throw new \Exception("Product X cannot be ordered in quantities greater than 3.");
        }
        
        // 规则2:周一最大数量为13
        if ($this->dateProvider->getCurrentDate()->format('N') === '1' && $quantity > 13) {
            throw new \Exception("On Mondays, order line quantity cannot exceed 13.");
        }
    }
}

然后在应用层调用时,先通过领域服务校验,再执行聚合根的变更方法:

// 应用层代码
$order = $orderRepository->findById($orderId);
$orderDomainService->validateOrderLineQuantityChange($order, $lineId, $quantity);
$order->changeOrderLineQuantity($lineId, $quantity);
$orderRepository->save($order);

如果规则数量较多,还可以把每个规则封装成独立的规则类(比如ProductMaxQuantityRule、MondayMaxQuantityRule),让领域服务组合这些规则,这样代码会更清晰,也更容易扩展。


内容的提问来源于stack exchange,提问作者Pakspul

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:09:02