起初,我相信允许我的一些实体访问存储库(即没有ORM的惰性加载)。后来我得出结论,我不应该这样做,我可以找到其他方法:
We should know our intentions in a request and what we want from the domain, therefore we can make repository calls before constructing or invoking Aggregate behavior. This also helps avoid the problem of inconsistent in-memory state and the need for lazy loading (see this article). The smell is that you cannot create an in memory instance of your entity anymore without worrying about data access.
CQS can help reduce the need for wanting to call the repository for things in our entities.
We can use a specification to encapsulate and communicate domain logic needs and pass that to the repository instead (a service can orchestrate these things for us). The specification can come from the entity that is in charge of maintaining that invariant. The repository will interpret parts of the specification into it's own query implementation and apply rules from the specification on query results. This aims to keep domain logic in the domain layer. It also serves the Ubiquitous Language and communication better. Imagine saying "overdue order specification" versus saying "filter order from tbl_order where placed_at is less than 30 minutes before sysdate" (see this answer).
It makes reasoning about the behavior of entities more difficult since the Single-Responsibility Principle is violated. If you need to work out storage/persistence issues you know where to go and where not to go.
It avoids the danger of giving an entity bi-directional access to global state (via the repository and domain services). You also don't want to break your transaction boundary.
据我所知,Vernon Vaughn在红皮书《实现领域驱动设计》中有两个地方提到了这个问题(注意:这本书完全得到了Evans的支持,你可以在前言中读到)。在第7章关于服务的章节中,他使用域服务和规范来解决聚合使用存储库和另一个聚合来确定用户是否经过身份验证的需求。引用他的话说:
根据经验,我们应该尽量避免使用存储库
(12)从聚合体内部,如果可能的话。
沃恩·弗农(2013-02-06)。实现领域驱动设计(Kindle位置6089)。培生教育。Kindle版。
在关于聚合的第10章中,在“模型导航”一节中,他说(就在他建议使用全局唯一id来引用其他聚合根之后):
通过标识引用并不完全阻止导航通过
该模型。有些人会在聚合中使用存储库(12)
查找。这种技术称为断开域模型
它实际上是惰性加载的一种形式。有一个不同的建议
但是,方法是:使用存储库或域服务(7)来查找
在调用聚合行为之前调用依赖对象。一个客户端
应用服务可以控制这个,然后分派到聚合:
他接着展示了一个代码示例:
public class ProductBacklogItemService ... {
...
@Transactional
public void assignTeamMemberToTask(
String aTenantId,
String aBacklogItemId,
String aTaskId,
String aTeamMemberId) {
BacklogItem backlogItem = backlogItemRepository.backlogItemOfId(
new TenantId(aTenantId),
new BacklogItemId(aBacklogItemId));
Team ofTeam = teamRepository.teamOfId(
backlogItem.tenantId(),
backlogItem.teamId());
backlogItem.assignTeamMemberToTask(
new TeamMemberId( aTeamMemberId),
ofTeam,
new TaskId( aTaskId));
}
...
}
他接着还提到了另一种解决方案,即如何在聚合命令方法中使用域服务以及双重分派。(读他的书大有益处,我怎么推荐都不为过。在你厌倦了无休止地在网上翻找之后,拿出你应得的钱去读这本书。)
然后我和Marco Pivetta @Ocramius进行了一些讨论,他向我展示了一些从域中提取规范并使用它的代码:
不建议这样做:
$user->mountFriends(); // <-- has a repository call inside that loads friends?
在域服务中,这是很好的:
public function mountYourFriends(MountFriendsCommand $mount) {
$user = $this->users->get($mount->userId());
$friends = $this->users->findBySpecification($user->getFriendsSpecification());
array_map([$user, 'mount'], $friends);
}