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

