《软件方法(下)》8.3.4.6 DDD话语“聚合”中的伪创新(2)

2024-06-03 11:20

本文主要是介绍《软件方法(下)》8.3.4.6 DDD话语“聚合”中的伪创新(2),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

DDD领域驱动设计批评文集

做强化自测题获得“软件方法建模师”称号

《软件方法》各章合集


《软件方法》最新pdf和epub文件:umlchina.com/url/softmeth2024.html


8.3 建模步骤C-2 识别类的关系

8.3.4 识别关联关系

8.3.4.6 DDD话语“聚合”中的伪创新

(3)aggregate root是伪创新

(续前文)

aggregate root错觉另一个可能的原因来自人类社会的直觉。

在8.3.4.3 关于“整体-部分”结构中,曾用部门结构来类比组合(聚合):

给各部门分配大任务,部门把任务分解,再分配给部门内的各小组……

这样的说法符合面向对象的思考,却与人类观察这个世界的直觉不符。

在直觉上,我们并不是观察到一个叫“项目部”的巨人,向着另一个巨人“财务部”吼一声,“喂,帮我结算这批临时工的工资!”,我们观察到的是一个人“项目部经理”向另一个人“财务部经理”发出请求“喂,帮我结算我部门这批临时工的工资!”

图片

图8-146 人类直觉观察不到这样的现象

因为作为观察者的我们是人,或者说,是一个人脑系统,所以观察者直觉上得到的第一个抽象是和自己同一级别的抽象,即系统和系统的交互:项目部经理-财务部经理。

这样的场景也会诞生类似aggregate root的错觉,“项目部经理”是“项目部”的根,“财务部经理”是“财务部”的根,他们负责和其他部门打交道。

而在面向对象的世界中,对象是有“生命”和“智慧”的,“项目部”可以像人一样,向“财务部”发消息。

即使部门中确实有经理职位而且系统需要维护这个信息,在分配责任的时候,也并不需要把“经理”对象作为责任分配的起点。

实际上,当前的绝大多数信息系统中,和现实中有生命的“人”对应的类,与其相关的逻辑所占比例非常少。承担系统关键责任、封装复杂逻辑、我们需要为之画出复杂状态机的类,往往在现实中对应的是无生命的事物,例如“订单”、“设备”、“房间”等。

即使将来信息系统发展到更高复杂度,“人”相关的逻辑所占比例依然会很少。人类建造的信息系统,封装的是人类对宇宙万事万物(当然包括人类自身)的认识或想象,而人类自身(甚至包括其他生命体在内)的内容在这宇宙万事万物中能占多少比例呢?

如果现实中没有生命,在信息系统里也没有“生命”的话,系统中应该只有映射“人”的类才配拥有操作,只有“人”才配作为所谓的“聚合根”了——这种现象在面向对象的初学者中很常见,例如认为“借书”是“馆员”的责任。

此处还有一个有趣的问题,留个读者思考:

假设一定要推行“聚合根”的革命性创造,根据上文所说,我们直觉的级别,是系统和系统的交互:项目部经理<->财务部经理。

如果向外一个级别,可以得到组织和组织的交互:项目部<->财务部。

如果向内一个级别,可以得到系统部件和系统部件的交互:项目部经理的嘴巴<->财务部经理的耳朵。

那么,“聚合根”项目部经理代表的是项目部还是项目部经理的嘴巴呢?

(4)associated的误用

我们第三次看“Domain-Driven Design”中的描述:

An AGGREGATE is a cluster of associated objects

一个AGGREGATE是一簇相关联的对象

前文讲到关联(Association)的概念时说过,关联是系统需要维护的关系。

我们用人来举例,如果是“a cluster of associated objects(一簇相关联的对象)”,类图可能如图8-147,各个部件之间存在关联:

图片

图8-147 部件之间存在关联

虽然现实中的人体结构有可能是像图8-147这样的,但目前我们的软件结构更倾向于像图8-148这样,部件只和整体关联,部件之间(最好)不存在关联:

图片

图8-148 部件只和整体关联

可能有的读者会想,图8-147有道理呀,这些部件之间有“兄弟”或“同事”关联呀!其实这些“兄弟”或“同事”是可以从整体-部分关联推算出来的冗余概念,系统不需要维护。

我们可以对比一下Vaughn Vernon在他的“Implementing Domain-Driven Design”[Vernon 2013]中的用词:

Is an Aggregate just a way to cluster a graph of closely related objects under a common parent? If so, is there some practical limit to the number of objects that should be allowed to reside in the graph? Since one Aggregate instance can reference other Aggregate instances, can the associations be navigated deeply, modifying various objects along the way?

聚合只是把在共同父对象之下紧密相关的对象图聚集在一起的一种方式吗?如果是这样,对于允许驻留在该图上的对象数目,有没有一些实际的限制?既然一个聚合实例可以引用其他聚合实例,那么关联是否可以向深处导航,沿途修改各种对象呢?

★“Implementing Domain-Driven Design”的中译本《实现领域驱动设计》在此处的翻译存在严重错误,参见《实现领域驱动设计》的译者其实没错?(二)。

Vaughn Vernon在此处用词非常准确。

描述“一簇对象”时,他用的词是“related(相关的)”。

怎么个相关?静态上,它们都是同一个整体的部件,动态上,它们中的部分或全部会在某个场景中由整体对象协调来完成任务,如图8-149:

图片

图8-149 部件在整体的协调下协作完成任务

同一段文字中,Vernon也在正确的地方使用了“association(关联)”一词,讨论沿着关联关系导航的深度问题。

读者可以回看8.3.1 类的关系中与“关系(relationship)”和“关联(association)”相关的内容,以对比related和associated。

(5)Eric Evans的葡萄隐喻错误

软件开发中,比UML、DDD更早使用Aggregate这个词的是SQL,各种Aggregate函数如SUM()、AVG()、MAX()、MIN(),用于统计表中的数据。Aggregate这个用词经常会让人产生错觉,以为是“累计”或“集合”。

Eric Evans在“Domain-Driven Design”书中用一串葡萄来隐喻Aggregate,如图8-150。这个隐喻的选择是有问题的,有可能把“聚合”当成了“集合”。

图片

图8-150 摘自Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans , 2003

一串葡萄就算有一亿颗,也只是同一个类“葡萄”的对象集合。在8.2.4.7 类命名用单数一节中说过,不需要对纯粹的对象集合建模。

图片

8-151 不需要建模纯粹的“葡萄集合”

★当然,此处适合践行某些领域驱动设计实践投资少、见效快、产量高、门槛低、仪式感十足的精神,可以把每个类都无条件地配一个“**们”或“**s”,类的个数瞬间翻倍。

如果一定要以葡萄做素材,以下这个隐喻可能更合理:

若干颗葡萄、若干个煎蛋、若干根油条……组成一份早餐,其中维生素C的质量不得少于蛋白质质量的1%……这个更有意义,如图8-152。

(注:该图仅从图8-150的葡萄延伸以作说明,图中的领域知识未经过调研,“葡萄”、“煎蛋”、“0.01”等抽象程度也不够,烦请忽略这些问题。)

图片

图8-152 更合理的隐喻

如果“对象集合”另有含义,演化成了另一个类的对象,那么这个“另一个类”不会仅仅是一个纯粹的集合,应该还会封装其他内容。

常被用于举例的“订单”和“订单项”,平时我们看到的例子可能如图8-153:

图片

图8-153 常见的订单-订单项类图

但这只是简化版,如果仅是这样,“订单”就没有必要存在了。之所以需要“订单”的概念,是因为还有“下单日期”、“顾客”、“收货地址”等概念,要通过“订单”组织起来,如图8-154。

图片

图8-154 扮演整体的类还会封装其他知识

Eric Evans选用这个葡萄图片,可能还搞错了另一个知识。这个知识不是软件开发知识,而是植物学知识。

植物学上有聚合果(Aggregate Fruit)的概念,如图8-155:

图片

图8-155 摘自百度百科“聚合果”词条

Eric Evans可能想到“Aggregate Fruit”这个术语,觉得葡萄是成串的,以为葡萄是“Aggregate Fruit”,于是把图片放上去了。

其实葡萄是单果,如图8-156。

图片

图8-156 摘自https://zhuanlan.zhihu.com/p/37538771

这篇关于《软件方法(下)》8.3.4.6 DDD话语“聚合”中的伪创新(2)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1026847

相关文章

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

C#中读取XML文件的四种常用方法

《C#中读取XML文件的四种常用方法》Xml是Internet环境中跨平台的,依赖于内容的技术,是当前处理结构化文档信息的有力工具,下面我们就来看看C#中读取XML文件的方法都有哪些吧... 目录XML简介格式C#读取XML文件方法使用XmlDocument使用XmlTextReader/XmlTextWr

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

oracle DBMS_SQL.PARSE的使用方法和示例

《oracleDBMS_SQL.PARSE的使用方法和示例》DBMS_SQL是Oracle数据库中的一个强大包,用于动态构建和执行SQL语句,DBMS_SQL.PARSE过程解析SQL语句或PL/S... 目录语法示例注意事项DBMS_SQL 是 oracle 数据库中的一个强大包,它允许动态地构建和执行

Ubuntu固定虚拟机ip地址的方法教程

《Ubuntu固定虚拟机ip地址的方法教程》本文详细介绍了如何在Ubuntu虚拟机中固定IP地址,包括检查和编辑`/etc/apt/sources.list`文件、更新网络配置文件以及使用Networ... 1、由于虚拟机网络是桥接,所以ip地址会不停地变化,接下来我们就讲述ip如何固定 2、如果apt安

Go路由注册方法详解

《Go路由注册方法详解》Go语言中,http.NewServeMux()和http.HandleFunc()是两种不同的路由注册方式,前者创建独立的ServeMux实例,适合模块化和分层路由,灵活性高... 目录Go路由注册方法1. 路由注册的方式2. 路由器的独立性3. 灵活性4. 启动服务器的方式5.

在不同系统间迁移Python程序的方法与教程

《在不同系统间迁移Python程序的方法与教程》本文介绍了几种将Windows上编写的Python程序迁移到Linux服务器上的方法,包括使用虚拟环境和依赖冻结、容器化技术(如Docker)、使用An... 目录使用虚拟环境和依赖冻结1. 创建虚拟环境2. 冻结依赖使用容器化技术(如 docker)1. 创

Spring排序机制之接口与注解的使用方法

《Spring排序机制之接口与注解的使用方法》本文介绍了Spring中多种排序机制,包括Ordered接口、PriorityOrdered接口、@Order注解和@Priority注解,提供了详细示例... 目录一、Spring 排序的需求场景二、Spring 中的排序机制1、Ordered 接口2、Pri

Idea实现接口的方法上无法添加@Override注解的解决方案

《Idea实现接口的方法上无法添加@Override注解的解决方案》文章介绍了在IDEA中实现接口方法时无法添加@Override注解的问题及其解决方法,主要步骤包括更改项目结构中的Languagel... 目录Idea实现接China编程口的方法上无法添加@javascriptOverride注解错误原因解决方

MySql死锁怎么排查的方法实现

《MySql死锁怎么排查的方法实现》本文主要介绍了MySql死锁怎么排查的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录前言一、死锁排查方法1. 查看死锁日志方法 1:启用死锁日志输出方法 2:检查 mysql 错误