谈DDD与贫血领域模型:再次为失血模型辩护 -Codecentric AG博客


在讨论如何在应用DDD时如何最好地实现我们的领域对象(最近变得越来越流行)的讨论中,一位同事向我指出了Martin Fowler关于Anemic Domain Models的文章(2003年)。马丁是我的英雄之一。我尝试阅读尽可能多的著作,并尽可能多地观看他的演讲,因为它们给了我深刻的见解和对所有可用选项的平衡理解,我认为这对于做出明智的软件设计决策至关重要。在前面提到的文章中,马丁使用了比较固执的语言来表示“贫血领域模型”是一种反模式。
现在,在2019年,即使多次重读这篇文章,我也很难将其视为普遍存在的问题。因此,我认为提出我的观点,即使这可能会扰乱DDD社区。

什么是贫血或失血领域模型?
在Martin开创性的EAA书(2002年)中,领域模型被定义为“结合了行为和数据的领域的对象模型”。这显然将其与实体对象区分开来,实体对象仅是存储在数据库中的数据的对象表示(关系型与否),而行为位于单独的类中。请注意,这种划分并不是真正的分层,而只是处理纯数据结构的过程。这是在像C这样的过程语言中工作的唯一方法。
因此,撇开不同的术语并简化事情,Anemic Domain Model基本上只是使用纯数据实体类的另一个词,其行为提取为我们可以称为操作行为的类

纯实体+操作有什么问题?
Martin承认他看到的主要痛苦是“映射到数据库的笨拙”。他认为只有在被映射的对象实际利用其潜力的情况下,这才是值得的。
但是,有两个因素可能使他的前提无效:(a)诸如JPA之类的ORM工具已经得到了很大的改进;您还可以将对象及其依赖项作为文档直接存储到例如MongoDB中。实际上,直接将实体映射到数据库或从数据库映射实体比直接使用JDBC结果集或手动映射数据要容易得多,也更清洁。
(b)更重要的是,如今,数据库模型通常仅由单个应用程序控制;幸运的是,团队通常通过与其他团队共享表进行数据库集成。因此,如果域模型需要发展,则无需进行任何协调就可以迁移数据库模式。我们可以使用统一的甚至是普遍存在的模型。

实体可以是真实的域对象吗?
很自然地可以向实体添加Bean验证约束:例如,您可以简单地向实体字段添加@Past注释,并且只要要存储它,验证就会自动完成。这可以看作是您用于例如的类型系统的扩展。无需额外的代码。

您还可以创建自定义约束,例如@Name,防止人们名字中出现笑脸和其他不适当的字符;您需要编写的验证器也是纯域逻辑代码。Bean验证程序可以处理相当大的复杂性:您可以(a)组合验证多个字段;(b)使用验证组,例如用于实体的不同状态;(c)为存储库和其他注入帮助程序类。例如,这使您可以检查房地产经纪人是否具有出售特定建筑物的法律要求的证书,具体取决于建筑物的位置和其他因素。

JPA实体类还可以具有其他域方法,例如,我们的Person类可以具有一种getAge从出生开始计算人员当前年龄的方法。或创建Customer对象可能会自动设置首次注册日期。

某些域方法需要存储库或其他帮助程序;例如,当(重新)计算订单金额时,我们可能必须获取商品价格。我们可以将我们需要的所有帮助者注入我们的实体中 ; 但是,将复杂的交互从Entity移入Operation类是一个很好的Clean Code习惯;尤其是 如果涉及多个实体;这样,测试也变得更加容易。

请注意,这些操作类仍然是域模型的一部分:它们应该是纯域逻辑,并且可能与实体位于同一包中。只有调用是不同的:没有调用order.calculateTotalPrice(),而是有一个操作行为OrderOperations类用于调用calculateTotalPrice(order)。

应用逻辑
全面的业务交易或事务有时会变得非常复杂。例如,关闭保险销售可能必须触发许多其他过程;我们甚至可能必须跨越多个有界上下文。通常,这通常被正确地提取到单独的应用程序层(有时称为服务层)中,该应用程序层“仅协调任务并委托工作”。您可以将其称为“ 事务脚本”。但是核心领域和应用程序逻辑之间的界线通常是模糊的,并且不同的人将其绘制在不同的位置。

构建前端后端时,前端必须显示其他数据,例如,将商品添加到订单中时的等级,这显然是应用程序逻辑;还是应该坚持每个步骤的多步骤创建操作,也显然是应用程序逻辑:决策是关于UX的,它由定义为表现层的可视化决策来完成:
Web应用程序区别于桌面应用程序界面,有不同针对终端的界面,但它可能具有相同的应用程序逻辑[ 6 ];应用程序逻辑可以通过subcutaneous tests轻松进行单元测试,而测试表现层则需要进行严格的集成设置。

Martin引用了Eric Evans的声明,即应用程序层“不包含业务规则或知识”。但是,业务交易中所涉及流程的协调不是业务领域知识吗?它不执行业务规则吗?我发现很难凭空得出应用程序和领域逻辑之间的区别。我更喜欢将诸如图层之类的设计工件的创建推迟到出现特定需求之前。(banq:作者将包含业务规则 与使用调用指挥业务规则 两个意思混淆,交通警察可以指挥交通,但是不必亲自开车,做协调的职责不必亲自参与某事的实作)

将数据与行为分离会导致人们在数据库一侧定义业务约束,这是一种过时思维,胖前端应用程序可以做这些工作。我可以理解为什么像Martin这样的面向对象的纯粹主义者会忽略这种方法(在数据库定义约束的方法)。但是它也有其优势:当我们仅分离复杂的行为时,可能不清楚我们应该在哪里寻找特定的操作。我们需要其他约定来定义何时做某事以及何时做另一件事。当我们从数据中分离出所有行为(验证逻辑以及计算和复杂的操作)时,事情就变得更加统一,并且可能更容易掌握和测试!

而且这绝不是一成不变的:进入互联网规模后,可能会导致诸如SOLID  项目之类的事情,在此  项目中,您的个人数据(图片,博客等)与您使用的应用程序严格分开(Facebook,Twitter等),因此您可以轻松切换到其他应用程序而不会丢失数据。这不仅是关于重新实现相同类型的应用程序;这是关于处理相同数据的不同类型的应用程序的。没有贫血域模型,这将是不可能的。(banq注:针对相同数据使用不同类型程序去处理,这是技术架构,不是模型设计,作者将技术模型与业务模型混淆,技术模型可以采取贫血模型,但是业务模型必须直接翻译业务领域的情况,不能将行为与数据分离,人们的思考方式很多是先名词后动词的方式)。

结论
我同意,过早地从失血域模型开始设计并将其声明为最佳实践是一种坏味道;但我不同意OO纯度是获得域模型资格的硬性要求。将操作类与实体类分开是一种有效的解决方案。只需将它们放在相同的“域模型”层中即可!有时有令人信服的理由将逻辑与数据分开;有时最好不要这样做。软件开发人员必须(使)能够(自己)看到何时最好做一个或另一个。并且,他们应该知道如何使用bean验证。

命名事物是计算机科学中的难事之一,它的确很重要。有人可能会争辩说,当对象贫血时,我们不应称其为领域模型。但是说DDD仅适用于纯OO范例,不仅排除并否认了上述实用的方法,而且还排除了例如函数式编程。那将是巨大的损失。我认为,Anemic失血域模型仍然可以称为适当的域模型-将它作为有效的工具带在您的腰带中是很好的。


 

核心领域 和 应用程序逻辑  怎么区别。他们的功能界限是怎么样的???

应用层的服务是委托协调职责,如同饭店的服务员,只是在顾客和厨房或老板之间协调,是一种业务协调者而不是业务决策者,核心领域才是业务决策者。应用层还会与基础设施交互。最好参考六边形架构或clean架构,比较清晰