域对象中的存储库
Repository within domain objects
我看到很多关于这个话题的讨论,但我无法得到令人信服的答案。一般的建议是不要在域对象中拥有存储库。聚合根呢?把操作组合对象的责任交给根不是对的吗?
例如,我有一个处理发票的微服务。发票是具有不同产品的聚合根。此服务不要求提供有关个别产品的详细信息。我有 2 个表,一个用于存储发票详细信息,另一个用于存储这些发票的产品。我有两个与表相对应的存储库。我在发票域对象中注入了产品存储库。这样做有错吗?
是的,我认为是错误的。
域应该匹配真实的业务模型,不应该关心数据是如何持久化的。即使数据在内部存储在多个表中,也不应该以任何方式影响域对象。
加载聚合根时,应一次性加载相关实体。例如,如果您使用的是 .NET,这可以通过 Entity Framework 中的 Include
关键字轻松实现。通过加载所有数据,您可以确保在任何给定时间都具有业务实体的完整表示,并且您不必再查询数据库。
相关实体中的任何更改都应在一个原子操作(通常使用事务)中与聚合根一起持久化。
根据 DDD 原则,我在你的问题中看到了一些错误。让我试着澄清一些概念来帮助你。
首先,您提到您有一个聚合根,即发票,然后是两个不同的存储库。拥有聚合根意味着聚合所包含的实体的任何更改都应通过聚合根执行。为什么?那是因为您需要满足一些适用于这些实体关系的业务规则(不变量)。例如,给定下一个业务规则:
Winning auction bids must always be placed before the auction ends. If a winning bid is placed after an auction ends, the domain is in an invalid state because an invariant has been broken and the model has failed to correctly apply domain rules.
这里有一个由拍卖和出价组成的聚合,其中拍卖是聚合根。
如果你有 BidsRepository
,你可以轻松做到:
var newBid = new Bid(money);
BidsRepository->save(newBid);
并且您在未通过定义的业务规则的情况下保存了出价。但是,只为聚合根设置存储库是在强制执行您的设计,因为您需要执行以下操作:
var newBid = new Bid(money);
auction.placeBid(newBid);
auctionRepository.save(auction);
因此,您可以在方法 placeBid
中检查您的不变量,如果有人想设置新的出价,则不能跳过它。之后您可以将信息保存到任意多个表中,这是一个实现细节。
其次,您说如果将存储库注入域 class 是错误的。这里有一个简单的解释:
The repository should depend on the object it returns, not the other way around. The reason for this is that your "domain object" (more on that later) can exist (and should be testable) without being loaded or saved (that is, having a dependency on a repository).
基本上您的设计表明,为了获得发票,您需要提供一个 MySQL/Mongo/XXX 实例连接,这是一个基础设施细节。您的域不应该知道它是如何持久化的。您的域了解拍卖和投标场景中的行为。
这些概念只是帮助您创建更易于维护的代码,并帮助您应用最佳实践,例如 SRP(单一职责原则)。
我看到很多关于这个话题的讨论,但我无法得到令人信服的答案。一般的建议是不要在域对象中拥有存储库。聚合根呢?把操作组合对象的责任交给根不是对的吗? 例如,我有一个处理发票的微服务。发票是具有不同产品的聚合根。此服务不要求提供有关个别产品的详细信息。我有 2 个表,一个用于存储发票详细信息,另一个用于存储这些发票的产品。我有两个与表相对应的存储库。我在发票域对象中注入了产品存储库。这样做有错吗?
是的,我认为是错误的。
域应该匹配真实的业务模型,不应该关心数据是如何持久化的。即使数据在内部存储在多个表中,也不应该以任何方式影响域对象。
加载聚合根时,应一次性加载相关实体。例如,如果您使用的是 .NET,这可以通过 Entity Framework 中的 Include
关键字轻松实现。通过加载所有数据,您可以确保在任何给定时间都具有业务实体的完整表示,并且您不必再查询数据库。
相关实体中的任何更改都应在一个原子操作(通常使用事务)中与聚合根一起持久化。
根据 DDD 原则,我在你的问题中看到了一些错误。让我试着澄清一些概念来帮助你。
首先,您提到您有一个聚合根,即发票,然后是两个不同的存储库。拥有聚合根意味着聚合所包含的实体的任何更改都应通过聚合根执行。为什么?那是因为您需要满足一些适用于这些实体关系的业务规则(不变量)。例如,给定下一个业务规则:
Winning auction bids must always be placed before the auction ends. If a winning bid is placed after an auction ends, the domain is in an invalid state because an invariant has been broken and the model has failed to correctly apply domain rules.
这里有一个由拍卖和出价组成的聚合,其中拍卖是聚合根。
如果你有 BidsRepository
,你可以轻松做到:
var newBid = new Bid(money);
BidsRepository->save(newBid);
并且您在未通过定义的业务规则的情况下保存了出价。但是,只为聚合根设置存储库是在强制执行您的设计,因为您需要执行以下操作:
var newBid = new Bid(money);
auction.placeBid(newBid);
auctionRepository.save(auction);
因此,您可以在方法 placeBid
中检查您的不变量,如果有人想设置新的出价,则不能跳过它。之后您可以将信息保存到任意多个表中,这是一个实现细节。
其次,您说如果将存储库注入域 class 是错误的。这里有一个简单的解释:
The repository should depend on the object it returns, not the other way around. The reason for this is that your "domain object" (more on that later) can exist (and should be testable) without being loaded or saved (that is, having a dependency on a repository).
基本上您的设计表明,为了获得发票,您需要提供一个 MySQL/Mongo/XXX 实例连接,这是一个基础设施细节。您的域不应该知道它是如何持久化的。您的域了解拍卖和投标场景中的行为。
这些概念只是帮助您创建更易于维护的代码,并帮助您应用最佳实践,例如 SRP(单一职责原则)。