富领域模型和ORM

26
马丁·福勒将贫血领域模型视为反模式。
将持久性模型作为领域模型也似乎严重偏差,因为存在对象关系不匹配。出于持久性和规范化的考虑,我们倾向于将类分解成非常小的部分,对这些类施加方法是愚蠢的。此外,持久性很少改变,但业务逻辑会发生相当大的变化。
因此,我们需要一个建立在持久性模型之上的领域模型(而不是相同的模型)。该领域模型将包含业务逻辑属性和方法。
但现在这些领域模型仍然在服务后面,为了将它们暴露给外部世界,我们需要将它们转换为DTO。
我们在进行如此多的映射。
  1. 持久性到领域模型
  2. 将领域模型转换为DTO以在服务之间传递
事情并没有结束,因为DTO可能需要映射到ViewModel中。
所有这些问题,重复验证逻辑的问题仍然存在,因为客户端需要实时验证。ViewModel 对验证一无所知,因此在 SPA 中,您被迫再次重写验证逻辑(通常使用 JavaScript 在客户端上)。
此外,服务本质上是无状态的(消息或 RPC 导向),因此我们在持久性、面向对象、过程化之间进行所有这些映射,有何好处?在大多数 IT 预算的实际术语中,您如何证明成本的合理性?
我了解完整的 DDD,包括聚合根、领域模型等,会很“酷”,但您如何证明成本和开发生产率的影响?
“反模式(或反模式)是社交或商业操作或软件工程中使用的一种模式,可能在实践中常用,但实际上是无效和/或具有反生产力的。”
如果是这样,那么 DDD 和 Rich Domain Model 是否符合上述反模式定义,而不是“精益”域模型。抱歉,我厌恶这个带有偏见的词,“贫血”。
通过保持领域模型的"精简",您实际上可以在不违反"抽象依赖原则"、"不要重复自己"以及耗时、繁琐和容易出错的数据映射过程中共享它,并且无需编写与之相关的单元测试(除非您想进行无单元测试的映射并希望取得良好效果)。
3个回答

9

简而言之,领域模型未定义清晰,可能是以数据库为中心的设计。

DDD的主要目的是在代码中对业务概念和流程进行建模。我真的怀疑你的业务概念和流程只是一堆属性。但如果确实如此,那么领域模型可以与持久性模型相同,因此您不必进行任何映射。

持久性模型建模了对象状态的存储方式。如果您不处于数据库领域,则领域和持久性模型不能具有相同的目的。我在DDD中看到的最大错误之一是将领域模型视为仍然受数据驱动的模型。在DDD中,您没有一个具体的数据库。您有存储库。没有一对多、多对多等关系。没有表格和行。只有试图尽可能准确地表示域的对象。

简单来说,领域关心建模业务行为,而持久性关心存储对象状态。我在这里看到两个不同的职责。

关于验证,您有2种类型:验证输入数据的格式,然后根据您在对象状态中更改的内容验证其他业务规则。

关于重复,我认为您指的是输入格式,但是像@EbenRoux所说,有一些机制可以帮助解决这个问题。在asp.net mvc中,大多数这些验证规则也包含了js版本。

让我告诉你一个小秘密:虽然服务的接口可以在领域中定义,但它们的实现可以位于持久性层中,从而使它们能够直接与存储交互。由于应用程序的其余部分以其抽象形式(接口)使用服务,因此只有DI容器才会知道这个脏秘密。

DDD不是为了酷炫,而是根据领域设计应用程序。我敢打赌,很少有人开发应用程序仅仅是为了成为数据库的用户界面。大多数旨在提供他们的软件服务,构建解决问题的虚拟产品。

这听起来怎么样,你想要一座砖房,但建筑商说:“抱歉,我的锯子只能用于木材。”好吧,不要使用锯子,使用另一种工具来帮助切割砖头。工具需要适应问题,而不是相反。


领域驱动设计中的领域是有状态还是无状态的?那么,领域是否应该公开其属性?如果不是,领域方法的参数和返回类型将是什么?DTOs?如果它确实公开其属性,那么这与贫血模型相比有何优势? - Alwyn
领域主要关注行为。当然,领域对象可以具有属性,但它不仅仅是属性。而且这些属性不仅仅是原始类型。如何直接在列中保存IFormattedContent格式化内容?我开始认为你当前的领域只是一个数据驱动的过程式代码集合,只是使用DDD术语。 - MikeSW
@Alwyn 领域的一部分是有状态的 - 实体正是拥有状态和身份的关键,这样我们就可以跟踪该状态的变化。另一方面,值对象是无状态的(如果您愿意,可以称之为状态冻结)。领域服务应明确无状态。 - guillaume31

9
看起来你把很多概念混淆了,责怪丰富的领域模型方法直接导致了某些问题是不正确的。
丰富的领域模型与分层架构无关,尤其是拥有丰富的领域模型并不意味着您需要多少层,应该在这些层之间交换什么数据结构以及它们应该如何映射。
丰富的领域模型与验证无关,并且对于客户端检查的需要和后端验证没有任何要求。
换句话说,使您的领域模型贫血化并将所有业务逻辑放到服务中不一定会节省大量样板DTO映射代码的编写工作,也不能消除客户端“双重检查”的需要(这是一个广为接受的最佳实践)。
这并不意味着您对完整的多层架构成本和复杂性的观点不正确。 您可能会对Mark Seemann在此帖子中探讨类似问题的内容感兴趣:http://blog.ploeh.dk/2012/02/09/IsLayeringWorthTheMapping.aspx

我认为你提供的文章总结了这个困境。除了我不相信通过网络发送领域模型,这似乎是DTO的工作。在DTO上添加行为并将其暴露给客户端毫无意义。因此,在我的看法中,该架构只能通过另一层抽象和映射来实现,除了他提到的三层之外。而这需要大量的映射!并不是说为了重用而将UI关注点引入领域的另一种方式就是正确的。我认为在两个极端之间有一个更“幸福”的中间地带。 - Alwyn
“移动更少的数据,事情很可能变得更简单。” - 这是他在文章结尾得出的结论,听起来像是精益模型。 - Alwyn
“在DTO上定义行为并将其暴露给客户端是没有意义的” - 现在我明白了你所说的丰富/贫血领域模型和分层之间的联系。你是否意味着应该尽可能剥离领域对象的业务逻辑,以便将它们直接发送到UI层而无需使用DTO?还是你指的是其他什么作为“精益模型”? - guillaume31
不,它可能有效,我只是认为你需要在领域模型和DTO之间再加一层。那个链接中的评论者之一也做出了同样的观察。我可以看到这个工作,但这是很多额外的开销,我无法确定这种权衡是否值得。 - Alwyn

5
首先,我认为你很难轻松地在客户端和服务器上避免重复验证逻辑。这不仅限于DDD。有一些机制可以减轻痛苦,但总是需要付出一些努力。
另一部分是整个映射业务 :)
您正在使用实现读取的方式。如果您正在对实体对象执行基于实体(在此处可能更多地涉及数据库术语 - 整个记录)的操作而不是基于任务的操作,则可能会认为需要读取实体才能编辑它。一个愚蠢的例子可能是客户打电话到呼叫中心来更改地址。操作员调用客户记录并编辑地址。这是基于实体的,并导致与并发性相关的典型问题,因为可能对同一记录执行2个操作(虽然不太可能)。这是UX设计的非常传统的方法:“编辑记录”。
与此相反,在屏幕上有一个按钮,上面写着“更改地址”。您只更改记录上的地址,尽管这似乎是相同的内容,但实际上是非常不同的。更改相同地址的2个操作的机会比更改相同记录的机会要小得多。如果需要,可以在此部分执行并发性检查。
现在,如果不读取域,那么会读取什么?数据从哪里来。这就是CQRS(命令/查询责任分离)的作用。过去它已经与事件源混淆/组合,但这不是必需的。您可以为应用程序创建一个简单的查询侧,专注于返回所需的数据。在C#中,如果是单个实例,则使用DataRow,如果是多个实例,则使用DataTable,并且对于更复杂的内容,则使用自定义DTO。可能还有一种方法可以使用匿名类型(尚未进行调查)。
因此:
域模型=操作/计算/写入 查询服务=读取
在Web应用程序等情况下,有时可以仅加载实体/聚合,因为它知道(或可能知道)您的域模型,但是智能客户端将是反模式。
正当性相当棘手,但归根结底是维护。如果您的方法没有减轻维护负担,那么很可能某些东西没有正确应用并需要进行一些重构。
DDD不仅涉及技术实现,尽管这可以大大推动适当的面向对象建模方向。我想其他想法也会反映在软件中,因此软件似乎是焦点。我们都希望看到橡胶与路面相接的地方。
像大多数事物一样,DDD可能做错了 :)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接