DDD 聚合具有重要不变量的潜在大型集合
DDD Aggregate with potentially large collection with important invariant
我明白聚合应该很小并且它们应该保护不变量。
我也知道在聚合中保留大量集合会影响性能。
我有一个用例,需要保护它的不变量,但也会导致大量集合。
聚合为 Vendor,它可以有多个活动的 Promotion(s)。每个 Promotion 都有 PromotionType、StartDate 和 EndDate。不变量是:
- 在任何时候每个 PromotionType
最多可以有一个促销
- 在任何时候最多可以有 2 个促销活动
public Vendor : Aggregate {
public Guid Id;
public List<Promotion> Promotions;
// some other Vendor props here
public void AddPromotion(Promotion promo) {
// protect invariants (business rules) here:
// rule_1: if 2 promotions are already active during any time between promo.Start and promo.End then throw ex
// rule_2: if during any time between promo.Start and promo.End there is promo with same Type then throw ex
// if all is ok (invariants protected) then:
Promotions.Add(promo);
}
}
public Promotion : ValueObject {
public PromotionType Type; // enum CheapestItemForFree, FreeDelivery, Off10PercentOfTotalBill
public DateTime Start;
public DateTime End;
}
正如我们所见,Promotions
集合会随着时间的推移添加新的促销而增长,旧的促销会过期。
解决方案 1)
一种可能性是使 Promotion
成为一个独立的聚合,包含 VendorId,但在那种情况下很难保护提到的不变量。
解决方案 2)
另一种可能性是有一个维护工作将过期(EndDate 已通过)移动到一些历史table,但它是 IMO 的臭解决方案。
解决方案 3)
另一种可能性是也使 Promotion
本身成为一个聚合,但保护域服务中的不变量,例如:
public class PromotionsDomainService {
public Promotion CreateNewVendorPromotion(Guid vendorId, DateTime start, DateTime end, PromotionType type) {
// protect invariants here:
// invariants broken -> throw ex
// invariants valid -> return new Promotion aggregate object
}
}
...但是在 PromotionsDomainService 中保护它(并返回聚合)我们冒着竞争条件和不一致的风险(除非我们应用悲观锁)。
在这种情况下推荐的 DDD 方法是什么?
这是一个有趣的案例,因为我一直在努力解决聚合根为什么需要实体的问题。我倾向于支持通过 id 引用其他聚合的聚合中的值对象,但我认为您可能在这里有一个实体。
一个解决方案可能是只能够在 Vendor
内注册促销活动,从而强制执行不变量。 VendorRepository
只会加载活动促销并将它们添加到 Vendor
。通过这种方式,它们可以随时过期,但存储库只会加载相关的促销活动。
对于开放式促销(您可能不一定有),您甚至可以在 Vendor
之外使它们过期,并且您的不变量仍应得到满足。
即使您使用 Promotion
作为值对象(这会起作用),您仍然可以遵循这种方法。
您的聚合应仅包含实现其目的所需的数据。阅读您的问题描述,我没有看到供应商需要任何过期的促销活动。因此,您只需要在集合中保留有效的促销活动。
在您的 AddPromotion 方法中,如果存在该类型的有效促销,您将 return 出错。如果没有该类型的任何促销,您将添加它,如果该类型的促销已过期,您将替换它。除非您有大量的促销类型(事实似乎并非如此),否则每种类型最多只能进行一次促销。看起来这将使集合保持在一个非常合理的大小。如果不是这种情况,请告诉我。
很有可能您需要过期的促销作为历史数据。但是这些应该在为此目的而设计的读取模型上,而不是在聚合中。为此,聚合可以为它接受新促销的每种类型发布一个事件,并且侦听器将对该事件做出反应并在历史促销中插入一条记录 table。
更新:
又看了一遍问题,我发现你甚至不需要为每种类型保留一个促销。集合中最多有 2 个促销,因此集合的大小最多为 2,除非我误解了。
我明白聚合应该很小并且它们应该保护不变量。 我也知道在聚合中保留大量集合会影响性能。
我有一个用例,需要保护它的不变量,但也会导致大量集合。
聚合为 Vendor,它可以有多个活动的 Promotion(s)。每个 Promotion 都有 PromotionType、StartDate 和 EndDate。不变量是:
- 在任何时候每个 PromotionType 最多可以有一个促销
- 在任何时候最多可以有 2 个促销活动
public Vendor : Aggregate {
public Guid Id;
public List<Promotion> Promotions;
// some other Vendor props here
public void AddPromotion(Promotion promo) {
// protect invariants (business rules) here:
// rule_1: if 2 promotions are already active during any time between promo.Start and promo.End then throw ex
// rule_2: if during any time between promo.Start and promo.End there is promo with same Type then throw ex
// if all is ok (invariants protected) then:
Promotions.Add(promo);
}
}
public Promotion : ValueObject {
public PromotionType Type; // enum CheapestItemForFree, FreeDelivery, Off10PercentOfTotalBill
public DateTime Start;
public DateTime End;
}
正如我们所见,Promotions
集合会随着时间的推移添加新的促销而增长,旧的促销会过期。
解决方案 1)
一种可能性是使 Promotion
成为一个独立的聚合,包含 VendorId,但在那种情况下很难保护提到的不变量。
解决方案 2) 另一种可能性是有一个维护工作将过期(EndDate 已通过)移动到一些历史table,但它是 IMO 的臭解决方案。
解决方案 3)
另一种可能性是也使 Promotion
本身成为一个聚合,但保护域服务中的不变量,例如:
public class PromotionsDomainService {
public Promotion CreateNewVendorPromotion(Guid vendorId, DateTime start, DateTime end, PromotionType type) {
// protect invariants here:
// invariants broken -> throw ex
// invariants valid -> return new Promotion aggregate object
}
}
...但是在 PromotionsDomainService 中保护它(并返回聚合)我们冒着竞争条件和不一致的风险(除非我们应用悲观锁)。
在这种情况下推荐的 DDD 方法是什么?
这是一个有趣的案例,因为我一直在努力解决聚合根为什么需要实体的问题。我倾向于支持通过 id 引用其他聚合的聚合中的值对象,但我认为您可能在这里有一个实体。
一个解决方案可能是只能够在 Vendor
内注册促销活动,从而强制执行不变量。 VendorRepository
只会加载活动促销并将它们添加到 Vendor
。通过这种方式,它们可以随时过期,但存储库只会加载相关的促销活动。
对于开放式促销(您可能不一定有),您甚至可以在 Vendor
之外使它们过期,并且您的不变量仍应得到满足。
即使您使用 Promotion
作为值对象(这会起作用),您仍然可以遵循这种方法。
您的聚合应仅包含实现其目的所需的数据。阅读您的问题描述,我没有看到供应商需要任何过期的促销活动。因此,您只需要在集合中保留有效的促销活动。
在您的 AddPromotion 方法中,如果存在该类型的有效促销,您将 return 出错。如果没有该类型的任何促销,您将添加它,如果该类型的促销已过期,您将替换它。除非您有大量的促销类型(事实似乎并非如此),否则每种类型最多只能进行一次促销。看起来这将使集合保持在一个非常合理的大小。如果不是这种情况,请告诉我。
很有可能您需要过期的促销作为历史数据。但是这些应该在为此目的而设计的读取模型上,而不是在聚合中。为此,聚合可以为它接受新促销的每种类型发布一个事件,并且侦听器将对该事件做出反应并在历史促销中插入一条记录 table。
更新:
又看了一遍问题,我发现你甚至不需要为每种类型保留一个促销。集合中最多有 2 个促销,因此集合的大小最多为 2,除非我误解了。