在领域驱动设计中,似乎有很多一致认为实体不应该直接访问存储库。

这是出自Eric Evans的《领域驱动设计》一书,还是来自其他地方?

对其背后的原因有什么好的解释吗?

编辑:澄清一下:我不是在谈论将数据访问从业务逻辑分离到一个单独层的经典OO实践——我是在谈论DDD中的具体安排,即实体根本不应该与数据访问层对话(即它们不应该持有对Repository对象的引用)。

更新:我把奖金给了BacceSR,因为他的答案似乎最接近,但我仍然对此一无所知。如果这是一个如此重要的原则,网上肯定会有一些关于它的好文章,不是吗?

更新:2013年3月,对这个问题的点赞表明人们对此很感兴趣,即使有很多答案,我仍然认为如果人们对此有想法,还有更多的空间。


当前回答

在理想的情况下,DDD建议实体不应该引用数据层。但是我们并不是生活在理想的世界里。域可能需要引用与它们没有依赖关系的其他业务逻辑域对象。实体以只读的目的引用存储库层来获取值是合乎逻辑的。

其他回答

虽然来晚了,但我还是会说点我的意见。

从性能角度来看,在域模型中抽象REST API操作的存储库和域服务可能是一个重大灾难。我认为无论是域服务(尽管在红皮书中有其他说法!),还是聚合都不应该尝试与它们合作,这两个概念应该只留在应用服务领域,无论你使用Layers还是Hexagon(端口和适配器),它都有与外部世界通信的唯一责任。

通过这种方式,所有昂贵的I/O通信都由一个应用程序服务分配并完全控制。它将:

防止任何类型的性能瓶颈。 在域模型中防止任何类型的全局访问(存储库是全局的)。

建立正确的对象图,在应用服务中使用正确的抓取策略,将纯内存中的对象传递给富域模型。惰性加载会潜入你的代码,并在最痛的地方打击你。

对我来说,这似乎是与OOD相关的一般良好实践,而不是DDD特有的。

我能想到的原因是:

关注点分离(实体应该与它们的持久化方式分离。因为根据使用场景的不同,可能存在多种策略,其中相同的实体将被持久化) 从逻辑上讲,实体可以在存储库操作的级别之下的级别中看到。较低级别的组件不应该具有较高级别组件的知识。因此,条目不应该有关于存储库的知识。

这是一个非常好的问题。我期待着就此进行一些讨论。但我想DDD的几本书里都提到了吉米·尼尔森斯和埃里克·埃文斯。我想通过示例也可以看到如何使用存储库模式。

BUT lets discuss. I think a very valid thought is why should an entity know about how to persist another entity? Important with DDD is that each entity has a responsibility to manage its own "knowledge-sphere" and shouldn't know anything about how to read or write other entities. Sure you can probably just add a repository interface to Entity A for reading Entities B. But the risk is that you expose knowledge for how to persist B. Will entity A also do validation on B before persisting B into db?

正如您所看到的,实体A可以更多地参与到实体B的生命周期中,这可以为模型增加更多的复杂性。

我猜(没有任何例子)单元测试将会更加复杂。

但是我确信总会有这样的场景:您想通过实体来使用存储库。你必须考虑每一种情况才能做出有效的判断。优点和缺点。但是在我看来,存储库实体解决方案有很多缺点。它一定是一个非常特殊的场景,优点抵消了缺点....

起初,我相信允许我的一些实体访问存储库(即没有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); 
}

这里有点混乱。存储库访问聚合根。聚合根是实体。这样做的原因是关注点的分离和良好的分层。这在小型项目中没有意义,但如果您在一个大型团队中,您会说:“您通过产品存储库访问产品。Product是实体集合的聚合根,包括ProductCatalog对象。如果你想要更新ProductCatalog,你必须通过ProductRepository。”

通过这种方式,您在业务逻辑和内容更新的位置上有非常非常清晰的分离。你不会有一个孩子独自编写了整个程序,对产品目录做了所有这些复杂的事情,当涉及到将其集成到上游项目时,你坐在那里看着它,意识到这一切都必须抛弃。这也意味着当人们加入团队,添加新功能时,他们知道该去哪里以及如何组织程序。

But wait! Repository also refers to the persistence layer, as in the Repository Pattern. In a better world an Eric Evans' Repository and the Repository Pattern would have separate names, because they tend to overlap quite a bit. To get the repository pattern you have contrast with other ways in which data is accessed, with a service bus or an event model system. Usually when you get to this level, the Eric Evans' Repository definition goes by the way side and you start talking about a bounded context. Each bounded context is essentially its own application. You might have a sophisticated approval system for getting things into the product catalog. In your original design the product was the center piece but in this bounded context the product catalog is. You still might access product information and update product via a service bus, but you must realize that a product catalog outside the bounded context might mean something completely different.

Back to your original question. If you're accessing a repository from within an entity it means the entity is really not a business entity but probably something that should exist in a service layer. This is because entities are business object and should concern themselves with being as much like a DSL (domain specific language) as possible. Only have business information in this layer. If you're troubleshooting a performance issue, you'll know to look elsewhere since only business information should be here. If suddenly, you have application issues here, you're making it very hard to extend and maintain an application, which is really the heart of DDD: making maintainable software.

对评论1的回应:好问题。所以并不是所有的验证都发生在领域层。夏普有一个属性“DomainSignature”,它可以做你想要的。它是持久性感知的,但是作为一个属性可以使域层保持干净。它确保在您的示例中,没有名称相同的重复实体。

But let's talk about more complicated validation rules. Let's say you're Amazon.com. Have you ever ordered something with an expired credit card? I have, where I haven't updated the card and bought something. It accepts the order and the UI informs me that everything is peachy. About 15 minutes later, I'll get an e-mail saying there's a problem with my order, my credit card is invalid. What's happening here is that, ideally, there's some regex validation in the domain layer. Is this a correct credit card number? If yes, persist the order. However, there's additional validation at the application tasks layer, where an external service is queried to see if payment can be made on the credit card. If not, don't actually ship anything, suspend the order and wait for the customer. This should all take place in a service layer.

不要害怕在服务层创建可以访问存储库的验证对象。让它远离域层。