DDD在子实体改变状态时强制聚合不变量

本文关键字:不变量 状态 实体 改变 DDD | 更新日期: 2023-09-27 18:12:48

我有一个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都应进行相应的修改。

DDD在子实体改变状态时强制聚合不变量

基于当前需求

解释您的需求,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<Sale>属性,而只是在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,并且您在新事务中异步处理该事件,则处理程序可以检查Sale日期是否仍然对合同有效。

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

同样,Contract.ChangeDate方法将发布ContractDatesChanged,处理程序将检查所有销售是否在合同日期内,并再次发出警报或进行调整。

这是DDD要求的"最终一致性"——你的所有销售必须在合同日期内完成的规则将得到满足……最终.

这就是为什么我说"挑战"需求——如果在这些情况下,允许销售日期超出合同日期并以适当的商业方式处理它真的会更好,那么你已经挑战了你的需求,并对该领域有了更深入的了解。