DDD 聚合具有重要不变量的潜在大型集合

DDD Aggregate with potentially large collection with important invariant

我明白聚合应该很小并且它们应该保护不变量。 我也知道在聚合中保留大量集合会影响性能。

我有一个用例,需要保护它的不变量,但也会导致大量集合。​​

聚合为 Vendor,它可以有多个活动的 Promotion(s)。每个 Promotion 都有 PromotionTypeStartDateEndDate。不变量是:

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,除非我误解了。