如果规则应该在持久层中实现,如何在域模型中表达业务逻辑规则?

How to express a business logic rule in domain model if that rule should be implemented in persistence layer?

我目前正在学习 DDD,似乎我误解了那里的一些核心思想。 假设有一个 "A" 聚合。它引用了其他几个实体和值对象。不可能(不是故意)以编程方式创建或更改它,因此它会变得无效。 "A" 可以通过构造函数创建。可以更改现有聚合。

public class A : IEntity {
    public int Id { get; }
    public string Name { get; }
    public B B { get; }
    public C C { get; }

    public A(string name, B b)
    {
        Name = name;
        B = b;
        // Some validation logic here to ensure that Name and B properties are correct. 
        // Throw an exception if business rules are not satisfied.
    }

    public void Edit(string newName, B newB)
    {
        // Some validation logic here to ensure that the aggregate with new values is correct.
        Name = newName;
        B = newB;
    }

}

还有一个"C"聚合。它被 "A" 引用。没有一个"A"(一对一关系)就无法创建。它还包含一些 "D" 个实体(一对多)。

public class D: IEntity
{
    public int Id { get; }
}

public class C: IEntity
{
    private readonly List<D> _ds = new List<D>();
    public int Id { get; }
    public IReadOnlyCollection<D> Ds => _ds;

    public void AddD(D d)
    {
        // Some validation logic for adding here.
        _ds.Add(d);
    }
}

所以现在我想介绍一个新的业务规则:只有当"A"没有"C"或者它的"C"没有任何[=时,我们才能编辑"A" 28=]秒。天真的实现非常简单:

public class A : IEntity 
{
   // just add this new method:
   public bool CanEdit() => C == null || !C.Ds.Any(); 

   // also invoke the above method in Edit() and throw if it's false.
}

不过,这种方法以后肯定会 "bite" 我。在“让我们看看 "A"s”用例中,我不需要在我的应用程序中加载任何 "C"s 或 "D"s。但是,我需要知道我是否可以编辑这些 "A" 个实体。所以这个规则的实际实现应该出现在持久层。

如何在我的域模型代码中表达这样的规则并在持久层中实现它?我应该将它放在存储库或域服务中吗?也许使用 CQRS 风格的查询更好?或者只是让大家更容易发表评论,解释在持久层应该实现什么规则?

我相信这是一个你本来就不应该有的问题。您的聚合不应直接引用其他聚合。当你听说一个 Aggregate 可以引用另一个 Aggregate 时,这只意味着它可以存储另一个聚合的 Id,而不是整个聚合。因此,您永远不应拥有具有 B 和 C 类型属性的聚合 A。相反,您应该拥有具有聚合 B 和 C 的 ID 的属性。

通过从您的聚合中移除这个问题,您移除了您的主要问题,但您会遇到一个新问题:您如何实际检查聚合 A 中依赖于聚合 C 的业务规则?

如果不讨论有关真实域的详细信息,这是不可能回答的。但请考虑以下两个选项:

  1. 你的聚合是错误的。当您拥有跨越多个聚合的业务规则时,这是您应该考虑的第一件事。为什么首先会发生这种情况?也许他们应该是一个单一的聚合,或者也许C的一部分属于A。聚合的重点是封装业务规则和他们需要评估的数据。如果聚合没有它需要的数据,那么它的设计就有问题。

  2. 您的聚合是正确的,但是 A 需要了解一些关于 C 的信息。这种情况经常发生。 A 负责折扣,C 负责用户忠诚度。如果用户拥有超过 1000 点成为高级会员,我们希望给予 10% 的折扣。按照您的方法,A 可以参考 C 并查看用户点数。但相反,让 C 封装让用户成为 Premium 的逻辑是更好的设计。因此,C 将跟踪用户积分。在某个时候,它将决定用户已成为 Premium 并引发 UserBecamePremiumEvent。聚合 A 将接收该事件并将 IsPremium 存储在其数据中。在下一次购买期间,A 将执行 "If user IsPremium, apply 10% discount"。