DDD - 每个实体的存储库还是一个存储库?
Posted
技术标签:
【中文标题】DDD - 每个实体的存储库还是一个存储库?【英文标题】:DDD - Repository per entity or one for everything? 【发布时间】:2012-03-10 18:23:35 【问题描述】:首先可能没有正确的答案,但我敢肯定有比我更了解的人能够提供帮助。
我有 3 个实体:用户、博客、帖子。
系统可以有任意数量的用户。
用户可以拥有任意数量的博客,但每个博客只有一个用户。
博客的帖子数量可以与用户发布的数量一样多,并且所有帖子都来自拥有该博客的同一用户(即,如果 John 拥有博客 Food,则只有 John 可以在此博客中发帖)。当然,每篇文章都有一个父博客。
然后我有用户个人资料页面,我想在其中显示所有用户详细信息、他所有博客的名称以及最近 5 个帖子。
然后我有一个博客页面,其中显示了博客的详细信息、所有者(用户)的名称和所有帖子的标题。
然后我有一个显示帖子详细信息、博客名称和所有者名称的帖子页面。
如你所见,我在它们之间都有关系,但它们都不能作为聚合。
在代码中定义实体并不难,我遇到的问题是定义存储库。我需要多少? 3 - 每个实体一个? 1 - 一切?如何执行查找?
例如获取用户页面中的最后 5 个帖子。用户没有对帖子的引用,而是拥有一个博客容器,其中每个博客又拥有一个帖子容器。我的存储库中是否应该有一个接受 UserID 并返回帖子列表的方法?或者它应该是一项服务?此外,我通常不执行所有数据的加载,而是延迟加载。通过检索现有的用户实体,我不会加载它的博客,除非需要它们(第一次访问时)。
提前致谢。
【问题讨论】:
“存储库”与“服务”到底是什么意思? IMO 这个问题对 SO 来说太宽泛了。 你可以在这里找到定义en.wikipedia.org/wiki/Domain-driven_design 是的,我有点担心这个问题可能对 SO 来说太宽泛了...... 【参考方案1】:我会创造:
用于处理帖子和 cmets 的 PostRepository BlogRepository 主要用于搜索 用户存储库如果您不打算支持 cmets,我将删除帖子存储库并处理 BlogRepository
中的帖子
我通常在使用后对存储库进行建模,并避免聚合嵌套(超过两个级别)。
例如获取用户页面中的最后 5 个帖子。用户没有对帖子的引用,而是拥有一个博客容器,其中每个博客又拥有一个帖子容器。
恕我直言,用户不应该拥有Blog
的容器。你有存储库来获取它。
我的存储库中是否应该有一个接受 UserID 并返回帖子列表的方法?
是的。
或者它应该是一个服务?
服务用于从使用领域模型的代码中删除业务逻辑。在你得到那种逻辑之前不要创建它们。
此外,我通常不会加载所有数据,而是使用延迟加载。通过检索现有的用户实体,我不会加载它的博客,除非需要它们(第一次访问时)。
没有将所有域模型链接在一起的属性可以避免延迟加载。尝试只拥有子聚合的属性。
【讨论】:
感谢您的评论!有趣的解决方案,我会考虑的。我也尽量避免嵌套聚合,但它并不总是像听起来那么简单。 谢谢,这样更干净,更有帮助!我只有一个问题:为什么用户不应该有博客容器?从您的回答来看,它清楚地简化了整个过程,但我不明白为什么它是错误的。我很确定您的心态不会像“因为拥有博客容器会使获取博客变得复杂并增加了实现延迟加载的需要,所以我决定不在用户实体中拥有博客容器”。这个决定背后应该有一些逻辑或规则。我很想知道。谢谢! KISS(保持简单和愚蠢)和 YAGNI(你不会需要它)实际上是我最喜欢的两个原则。在用户模型中拥有博客属性会使事情变得更加复杂,因为您可以直接使用BlogRepository
。没有它也使将来的重构更容易,因为您只有一种访问博客模型的方法(通过存储库)。
很抱歉我很挑剔 :) 但是,为什么在博客中收集帖子?或者为什么要从 EntityA 到 EntityB?为什么不直接创建存储库并使用它们,而不是直接引用其他实体/实体集合?
因为当您的应用程序增长时,User
对象将具有 50 个不同的引用属性,因为用户与系统中的所有事物都有关系。 Blog
依赖于它的帖子(一个空的博客毫无价值),但用户不依赖于它的博客。【参考方案2】:
在决定是否拥有存储库之前,我会先查看您的实体的重要性。
从测试驱动开发方面来看,我会为每个实体创建一个存储库。通过这种方式,我可以获得 IUserRepository、IBlogPostRepository 等的假实现。这种方法促进了单元测试,并允许我用另一种存储库的实现替换。
然后,我会将我的存储库分组到我认为合适的服务中。这样做的一种方法是以适合您要求的方式编写用例和组。
例如,我可以说用户可以执行以下操作:
-
查看帖子
提交帖子
编辑帖子
我现在可以创建一个 IUserService 来查看、提交和编辑帖子。此服务将聚合(分组)我的存储库。
最后,我将能够做到以下几点。
var blogPost = new BlogPost();
ServiceFactory.Resolve<IUserService>(c => c.SubmitPost(blogPost));
现在,这是执行此操作的一种方式。
或者,您可能有一个 IBlogPostService 会暴露非常相似的行为,或者您可以只使用存储库而不实现聚合服务。
我想说的是,你不应该在小细节上挂断。通常有多种方法可以实现类似的行为,而你将学习的唯一方法就是实践。
让您的存储库可重用,但问问自己您是否过度设计了某些东西,即我是否会单独使用这个存储库?一旦您的存储库很少,请将它们包装在聚合服务中。
如果您的服务有超过 3 个(阈值完全取决于您)存储库,则考虑将此服务重构为两个独立的服务。
我希望这会有所帮助。
【讨论】:
【参考方案3】:用户通常是典型的聚合根。用户聚合可能包含用户偏好、用户统计数据,甚至可能包含博客。
我还认为 Post 是聚合根的一个很好的候选者,它周围有很多吸引人的东西:评论、标签、类别...然后你可以为你的页面使用 PostRepository.LoadByBlog() 或 PostRepository.LoadByUser() .
现在这只是从您所描述的情况中推断出的一种可能性。每个域都是独一无二的,您可能拥有我们不知道的改变游戏规则的信息。
【讨论】:
感谢您的评论!但请坚持我描述的域。当您添加其他实体(标签、类别)时,域会发生巨大变化,这并不好。我描述的域是我拥有的域,这是我提出问题的域。我故意选择相对简单的域,因为我不想复杂化(还没有),因为我目前缺乏 DDD 的经验来制作更复杂的域。 即便如此,我仍然拥有这 2 个聚合根和相应的存储库。假设您将用户 ID 作为输入(来自 URL 或类似的东西),UserRepository.LoadById() 将显示用户详细信息。从那里您可以导航到用户的博客以获取他们的标题,并且 PostRepository.LoadLatestFiveByUserId() 将为您提供 5 个最近的帖子。然后您可以将 PostRepository.LoadAllByBlog() 和 PostRepository.LoadById() 用于其他页面。 您猜对了 URL。我对相同的解决方案(或多或少)很坚持,但在我的解决方案中我也有 BlogRepository,但是很多代码是重复的(即 BlogRepository.GetById() 将不得不加载博客和用户,而 UserRepository.GetById( ) 将加载用户和博客,因此其相同的功能会复制到 2 个存储库)。 skwee,关于您的最后评论。是的,您可以自动执行此操作并在 var user = Repository.GetById以上是关于DDD - 每个实体的存储库还是一个存储库?的主要内容,如果未能解决你的问题,请参考以下文章
.NET 的 SOLID DDD ORM 请求(使用干净的实体和存储库)