【问题标题】:DDD enforce aggregate invariants when a child entity changes state当子实体改变状态时,DDD 强制聚合不变量
【发布时间】:2016-10-13 08:01:57
【问题描述】:

我有一个Contract 实体,它有一个DateRange(dateFrom, dateTo) 属性和一个Sales 集合。

每个Sale 还具有一个DateRange 属性,该属性必须位于ContractDateRange 的边界内。

在更改Sale 的日期时,执行上述不变量的正确方法是什么?

public class Contract : Entity
{
    public DateRange Dates { get; private set; }
    public ICollection<Sale> Sales { get; private set; }
}

public class Sale : Entity
{
    public DateRange Dates { get; private set; }

    public void ChangeDates(DateRange dates)
    {
        Dates = dates;
    }
}

编辑

Contract 日期可以随时更改,因此每个Sale 都应相应修改。

【问题讨论】:

    标签: c# domain-driven-design


    【解决方案1】:

    根据您当前的要求

    解释您的要求,Contract 是聚合根,SaleContract 聚合中的一个实体。由于要求任何销售日期必须位于一组合同日期内,因此对销售日期的任何更改都必须由合同管理,因此它可以首先检查合同日期。

    为此,您可以在Contract 上使用一个方法,例如:

    public void ChangeSaleDate(long SaleId, DateRange dates)
    {
        if (this.Dates.Surround(dates))
        {
            var sale = this.Sales.First(s => s.Id == SaleId);
            sale.ChangeDates(dates);
        }
        else
        {
            throw new ArgumentException("New Sale dates must be between ...", "dates");
        }
    }
    

    这假设您有一个 SaleId - 或其他识别合同中销售的方式,并且您已在 DateRange 上实施了一个 Surround 方法来支持这种检查。

    根据您的项目结构,您还可以将Sale 上的ChangeDates 方法标记为internal,以确保您不会意外地从应用程序服务中调用它。

    从您的评论来看,确实,这种机制可以导致聚合根 (Contract) 上的大量方法,因为它强制执行适用于合同中“所有”销售的不变量。因此,此类情况可能会提示挑战要求...

    挑战要求

    DDD 有助于聚合之间的“最终一致性” - 由于聚合定义了一致性边界,如果您想定义跨越边界的规则,您必须接受该规则可能始终申请。

    另一种实现方式是使Sale 成为自己的聚合。在这种情况下,Contract 上不会有 ICollection&lt;Sale&gt; 属性 - 而Sale 上只有ContractId 属性,并且每次销售都会获得自己的全球唯一标识符。

    但是,这种技术的可行性取决于是否允许更改合同日期,以及更改时会发生什么......为了说明:

    要更改销售日期,您可以使用ContractRepository 获取Contract,使用SaleRepository 获取Sale,并可能将合同传递给Sale:

    public void ChangeDate(Contract contract, DateRange dates)
    {
        if (contract.Id != this.ContractId)
            throw new ArgumentException("wrong contract", "contract");
    
        if (!contract.AreSaleDatesValid(dates))
            throw new ArgumentException("wrong dates", "dates");
    
        this.Dates = dates;
    }
    

    这里的风险,因为您的合同和销售在交易上不一致,取决于合同日期是否可以更改。

    如果没有,那么这种方法简单可行,并确保您可以直接访问 Sales。

    但是,如果可以,那么风险是合同日期可能会在更改的同时您正在更改销售日期,因此您的规则将暂时被打破。

    但是,这是域事件可能提供帮助的地方。如果您的 Sale.ChangeDate 方法发布了一个事件 SaleDatesChanged 并且您在新事务中异步处理该事件,那么处理程序可以检查销售日期对于合同是否仍然有效。

    接下来会发生什么取决于您的业务需求 - 提醒人工审核,还是自动更改销售日期以适应新的合同日期?

    类似地,Contract.ChangeDate 方法会发布 ContractDatesChanged,而处理程序会检查所有销售是否在合同日期内,并再次提醒或调整。

    这是 DDD 要求中的“最终一致性”——您的所有销售必须在合同日期内的规则最终会得到满足。

    这就是我说“挑战”要求的原因 - 如果在这些情况下允许销售日期超出合同日期并以业务适当的方式处理它真的会更好,那么您已经挑战了自己的要求,并对领域有了更深入的了解。

    【讨论】:

    • 我也想过这个方案,但是合约实体会有很多像上面这样的功能。就 ddd 而言,这是否正确?即使使用 internal 修饰符,它也不会阻止其他开发人员直接使用该方法。我也阅读了有关领域事件的建议,但我还没有找到完整的实施。
    • 那么测试呢?内部没有帮助。
    • 扩展了我的答案,以说明域事件如何提供帮助 - 如果您放宽要求 - 以及如果您确实有“必须始终为真”的要求,为什么必须通过 Contract 对象。跨度>
    • 您可以利用InternalsVisibleTo 程序集属性来允许测试项目的可见性。然而,更好的是,由于业务规则包含在 Contract 类中,您可以通过 Contract 类对其进行测试。在测试时不模拟域实体更容易 - 而是专注于测试整个聚合(包括ContractSale),因此使用 Contract 方法,然后断言 Sale 状态。
    • 合同日期确实可以随时更改,因此应更改销售额以使合同汇总保持一致。
    猜你喜欢
    • 1970-01-01
    • 2016-03-25
    • 2020-10-19
    • 1970-01-01
    • 2020-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-07-17
    相关资源
    最近更新 更多