5

假设我们有一个 Order 类型的聚合根实体,它将客户和订单行关联起来。当我考虑订单实体时,更自然地将其概念化为没有 ID 就没有定义。没有 Id 的订单似乎比订单更好地表示为订单请求。

要将订单添加到存储库,我通常会看到人们在没有 ID 的情况下实例化订单,然后让存储库完成对象:

class OrderRepository
{
    void Add(Order order)
    {
        // Insert order into db and populate Id of new order
    }
}

我喜欢这种方法的地方在于,您正在向 OrderRepository 添加一个 Order 实例。这很有意义。但是,订单实例没有 Id,并且在存储库的消费者范围内,订单没有 Id 对我来说仍然没有意义。我可以将 OrderRequest 定义为 order 的实例并将其添加到存储库中,但这感觉就像从橙子中派生出一个苹果,然后将其添加到橙子列表中。

或者,我也看到了这种方法:

class OrderRepository
{
    Order AddOrder(Customer customer)
        // It might be better to call this CreateOrder
    {
        // Insert record into db and return a new instance of Order
    }
}

我喜欢这种方法的地方在于,没有 ID 的订单是未定义的。存储库可以在创建和返回订单实例之前创建数据库记录并收集所有必需的字段。这里闻起来的事实是,您实际上从未将订单实例添加到存储库中。

无论哪种方式都有效,所以我的问题是:我是否必须接受这两种解释中的一种,或者是否有最佳实践来模拟插入?

我发现这个答案是相似的,但对于值对象: 我应该如何将对象添加到由聚合根维护的集合中。当涉及到一个值对象时,没有混淆,但我的问题涉及一个具有从外部源(自动生成的数据库 ID)派生的身份的实体。

4

1 回答 1

5

我想首先排除第二种方法。它不仅看起来违反直觉,而且还违反了几个良好的设计原则,例如命令-查询分离最小意外原则

其余选项取决于域逻辑。如果域逻辑规定没有 ID 的订单是没有意义的,那么 ID 是订单的必需不变量,我们必须对其建模:

public class Order
{
    private readonly int id;

    public Order(int id)
    {
        // consider a Guard Clause here if you have constraints on the ID
        this.id = id;
    }
}

id请注意,通过将字段标记为readonly我们已使其成为不变量。对于给定的 Order 实例,我们无法更改它。这完全符合域驱动设计实体模式。

您可以通过将 Guard Clause 放入构造函数来进一步强制执行域逻辑,以防止 ID 为负数或零。

到目前为止,您可能想知道这将如何与数据库中自动生成的 ID 一起工作。好吧,它没有。

没有好的方法可以确保提供的 ID 尚未在使用中。

这让您有两个选择:

  • 将 ID 更改为 Guid。这允许任何调用者为新订单提供唯一的 ID。但是,这也需要您使用 Guid 作为数据库键。
  • 更改 API,以便创建新订单不使用 Order 对象,而是使用您建议的 OrderRequest - OrderRequest 可能与 Order 类几乎相同,只是减去了 ID。

在许多情况下,创建一个新订单是一项业务操作,无论如何都需要特定的建模,所以我认为区分这种情况没有问题。尽管 Order 和 OrderRequest 在语义上可能非常相似,但它们甚至不必在类型层次结构中相关。

我什至可以说它们应该相关,因为 OrderRequest 是一个值对象,而 Order 是一个实体

如果采用这种方法,AddOrder 方法必须返回一个 Order 实例(或至少返回 ID),否则我们无法知道我们刚刚创建的订单的 ID。这导致我们回到违反 CQS 的问题,这就是为什么我更喜欢 Guids for Entity IDs的原因。

于 2010-01-19T20:57:32.877 回答