【问题标题】:DDD - Change entity inside aggregateDDD - 更改聚合内的实体
【发布时间】:2019-12-30 12:58:29
【问题描述】:

阅读DDD - Modifications of child objects within aggregateUpdate an entity inside an aggregate 后,我仍然对在聚合中实现实体更改感到困惑。据我了解,聚合根代表整个(或整个聚合),并将“命令”更改委托给其余部分。

这最后一部分,委托给其余部分造成了一些问题。在下面的示例中,我想更改特定订单行的数量。我正在处理根“订单”并告诉它更改由本地标识符标识的订单行的数量。

当满足所有业务规则时,可以创建事件并将其应用于聚合。目前,所有事件都应用于聚合根,我认为这是一个很好的做法,因此所有命令都针对根,这会改变聚合的状态。此外,聚合根是唯一创建事件的对象,让全世界都知道发生了什么。

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;
    }
}

但是,当我应用此实现时,我遇到了一个问题,因为您注意到检查 $quantity 值的业务规则位于两个类中。这是故意的,因为我真的不知道最好的位置。该规则仅适用于 OrderLine 类,因此它不属于 Order。但是当我从订单中删除它时,将创建无法应用的事件,因为并非所有业务规则都得到满足。这也是不想要的。

我可以在 OrderLine 类中创建一个方法,例如:

    public function canChangeQuantity(int $quantity)
    {
        if ($quantity < 0) {
            return false;
        }
        return true;
    }

将 OrderLine 中的方法更改为:

    public function changeQuantity(int $quantity)
    {
        if ($this->canChangeQuantity($quantity) < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }

        $this->quantity = $quantity;
    }

现在我可以将 Order 类中的方法更改为:

    public function changeOrderLineQuantity(string $id, int $quantity)
    {
        $orderLine = $this->orderLines[$event->getId()];
        if ($orderLine->canChangeQuantity($quantity)) {
            throw new \Exception("Quantity may not be lower than zero.");
        }

        $this->applyChange(new OrderLineQuantityChangedEvent(
            $id, $quantity
        ));
    }

确保业务逻辑在它所属的地方,而不是在两个地方。这是一种选择,但如果复杂性增加并且模型变得更大,我可以想象这些实践会变得更加复杂。

现在我有问题: (1) 你如何应对从根开始的聚合深处的变化? (2) 当业务规则增加时(例如,最大数量为 10,但在星期一增加了 3 个,产品 X 最大为 3 件)。为聚合根上的每个命令/方法提供验证这些业务规则的域服务是否是良好做法?

【问题讨论】:

    标签: domain-driven-design aggregateroot


    【解决方案1】:

    我有一个问题,您注意到检查 $quantity 值的业务规则位于两个类中。

    从“面向对象”的角度来看,Order::changeOrderLineQuantity($id, $quantity) 是一个消息。消息具有架构是正常的,架构会限制任何给定字段中允许的值范围。

    所以这里的代码:

    public function changeOrderLineQuantity(string $id, int $quantity)
    {
        if ($quantity < 0) {
            throw new \Exception("Quantity may not be lower than zero.");
        }
    

    是消息验证的示例,您正在检查数量是否在允许的值范围内,因为通用int 原语过于宽松。

    使用强类型语言的领域建模人员在这里经常会引入一个新的类型,也就是ValueObject,它可以对具有范围限制的数据进行建模。

    // Disclaimer: PHP is not my first language
    class Quantity {
        public function __construct(int $quantity) {
            if ($quantity < 0) {
                throw new \Exception("Quantity may not be lower than zero.");
            }
            $this.quantity = quantity
        }
        //  ...
    }
    

    在简单的情况下,Orders::changeOrderLineQuantity(...) 所理解的QuantityOrderLineQuantityChangedEvent(...) 所理解的Quantity 相同的域概念与OrderLine::changeQuantity(...) 所理解的Quantity 相同的域概念,并且因此,您可以在任何地方重复使用相同的类型;因此,类型检查器确保满足正确的约束。

    编辑

    正如Eben Roux 在该问题的 cmets 中所指出的,这里的Quantity 应该被理解为某种通用的通用类型。相反,它特定于 Orders 和 OrderLines 的上下文,以及共享相同约束的代码的其他部分出于相同的原因

    一个完整的解决方案可能在不同的命名空间中有几种不同的Quantity 类型。

    【讨论】:

    • 您如何考虑业务规则随时间的变化。当您构建或反序列化以这种方式实现的 ValueType 时,您是否应用了业务规则?如果是这样,如果业务规则的更改意味着现有数据不再“有效”,您会怎么做?
    • 这是一个很好的问题。简短的版本是您需要仔细考虑“不可表示”状态和“不可到达”状态之间的区别。 Unreachable 是“容易的”,因为这完全取决于域模型。如果所有受影响的数据都已经干净,则“无法表示”是可管理的。对于其余的? “这取决于” - 您开始对您的选择进行成本与收益分析。
    • 感谢您的解释!我在其他点上使用了值对象,但我确实清楚地看到了这部分。我试图创建一个与我的情况非常接近的示例,以便我可以复制这些概念,但是实体也允许这样做吗?我有一个 cmets 任务,用户可以在有或没有评论的情况下启动任务。我目前拥有的方法是 start(VOUser $user, string $comment) 在我将它们组合在一个实体中的方法中,但也允许已经向 start 方法提供实体注释?
    • 我不会——消息通常是不可变的,而实体通常是可变的,因此当您在消息中包含可变实体时会产生一些冲突。但这是来自一般原则的论点——在适当的情况下,这种模式可能是“好的”。
    • 我同意这里的粗略。我只是补充一点,在多个地方检查相同的业务规则可能不是最好的主意。您可以放弃检查changeOrderLineQuantity,因为这里的不变量属于OrderLine,因此事件处理会拾取它。通用值对象可能会使事情变得更容易,尽管数量很可能是decimal,并且某些数量可能需要为负数,有些可能不是 0。这取决于用途。但总体思路仍然存在。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-11-05
    • 1970-01-01
    • 2019-06-29
    • 2021-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多