根据您当前的要求
解释您的要求,Contract 是聚合根,Sale 是 Contract 聚合中的一个实体。由于要求任何销售日期必须位于一组合同日期内,因此对销售日期的任何更改都必须由合同管理,因此它可以首先检查合同日期。
为此,您可以在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 并且您在新事务中异步处理该事件,那么处理程序可以检查销售日期对于合同是否仍然有效。
接下来会发生什么取决于您的业务需求 - 提醒人工审核,还是自动更改销售日期以适应新的合同日期?
类似地,Contract.ChangeDate 方法会发布 ContractDatesChanged,而处理程序会检查所有销售是否在合同日期内,并再次提醒或调整。
这是 DDD 要求中的“最终一致性”——您的所有销售必须在合同日期内的规则最终会得到满足。
这就是我说“挑战”要求的原因 - 如果在这些情况下允许销售日期超出合同日期并以业务适当的方式处理它真的会更好,那么您已经挑战了自己的要求,并对领域有了更深入的了解。