DDD领域驱动设计批评文集
“软件方法建模师”不再考查基础题
《软件方法》各章合集
我写了一篇文章,批评付施威的《DDD诊所——聚合过大综合症》(以下简称《DDD诊所》),文章是《你的医书是假的!批评付施威的《DDD诊所——聚合过大综合症》》(以下简称《你的医书是假的》)。
一、《你的医书是假的》关键内容
《DDD》诊所说,下面这个是不变式:
图1 《DDD诊所》截图
我在《你的医书是假的》中提出以下批评:
(1)不变式是类的不变式。《DDD诊所》作者把它放在关系上是不对的,说明该作者没有理解不变式。
(2)《DDD诊所》作者引用的《领域驱动设计》译文的翻译很糟糕(参见《猴子掰玉米?比较不同版《领域驱动设计》说“不变式”和“聚合”》),关键内容的意思译反了,而《DDD诊所》作者并无察觉,反而整段引用作为依据。
(3)不变式不是用来描述像图中的“订单总价=sum(订单行总价)”这样的内容的。如果这样的约束被严格遵守,说明“订单”的“总价”属性在逻辑上是冗余的,应该删掉。既然“总价”属性没了,图1的所谓“不变式”自然也就没有了。
也就是说,改变订单属性值(包括加/删订单行以及其他操作)的时候,不需要判断某个含有“总价”属性的表达式是否为真——实际上也没法判断,“总价”属性都没有了嘛。
接下来,我再针对图1做进一步的阐述,帮助进一步理解不变式,并评点在《你的医书是假的》文章下面的留言。
二、等号和赋值号没搞混吧?
不变式是一个布尔表达式。
我在这里提这一点,到底是什么意思呢?
意思是:
如果发现有“不变式”表示为相等关系的表达式,那就要警惕一下,建模人员是不是把“=”当成赋值来理解了。
例如,图1的式子“订单总价=sum(订单行总价)”,有可能就会被误解成“把订单项的价钱之和赋值给订单总价以同步这两边的值”。
读者可以注意到,我在《你的医书是假的》画的图例中,没有相等关系的不变式。
读者还可以对比下面的例子:
图2 三角形及其不变式
这个还行,对吧?
那读者再看这个:
图3 错例:等边三角形及其不变式?
这个是不是比较诡异?不过,这样的图倒是符合DDD伪创新的口味:投资少、见效快、门槛低、产量高、仪式感十足。
注意,我在图3中用“==”表示相等关系,而不是用“=”。其实这是违反OCL语法的,OCL里就是“=”表示相等关系,和SQL类似,但考虑到上面说的赋值误解(特别是显示在类图上时),我倒是觉得OCL应该改一下。
那么,可不可以有相等关系的不变式?可以有,但很少。相等关系表达式,经常是出现在前置条件和后置条件,而不是不变式。
什么时候可以有相等关系的不变式,这个就涉及到相当多的知识,不是本文能装得下的了,感兴趣的读者可以关注我的《软件方法》。
三、“订单总价=sum(订单行总价)”的另一个问题
这个问题在《你的医书是假的》中没指出来,在这里补上:
“订单总价=sum(订单行总价)”这个所谓的“不变式”,和图1中的属性名、关联角色名对不上。
图1中的属性名、关联角色名没有“订单总价”和“订单行总价”,只有“订单.总价”、“订单行.总价”——这也说明《DDD诊所》作者当时没有理解不变式是干什么用的。
四、不变式在哪里实现?
上面说了,不变式是一个布尔表达式,它只是说,对象的属性值不能违反这个约束,至于如何保证这一点,它没有做任何指定或暗示。
例如,把每一个不变式封装成一个方法,如果某个操作会修改某不变式中所涉及到属性的值,就调用相应方法来检查——这个工作可不轻松。
如果所用的实现技术支持前置条件、后置条件、不变式等契约编程,例如.NET中提供的Contract类,使用该类提供的服务即可。
五、类似“sum(订单行总价)”的运算会出现在哪里?
可能会有人问这样的问题。
类似“sum(订单行总价)”的运算,像其他针对各个属性的各种运算一样,并没有什么特别,会出现在类的某个操作或多个操作里。
什么操作?正确的回答是“信息不足,不知道”,可能“订单”专门有一个“计算总价”的操作,“sum(订单行总价)”藏在该操作的实现中,也可能“订单”根本不需要有“计算总价”的操作,“sum(订单行总价)”藏在“订单”的某个更复杂的操作的实现中。
不管怎么样,这和图1那个相等关系的“不变式”不存在什么有规律的对应关系。
六、关于付施威的解释
《DDD诊所》作者付施威在《你的医书是假的》的评论区留言解释:
图4 《DDD诊所》作者付施威的解释
先说解释中的第3点:
如果还有“订单总价<3000”这样的约束,之前的结论也没有变化,只不过“订单”的不变式多了一个:
图5 加上订单总价<3000后的类图变化(注:我把类名和属性名改成订单项.价钱)
再说解释中的第1点:
付施威谦虚地解释“DDD诊所是内部活动”、“主要职责是被人研究”,但下面的海报传递出来的信息似乎是不一样的。
图6 活动海报1
图7 活动海报2
开直播以专家身份传道授业解惑没问题,但被指出问题了,又谦虚地说是学习体会,这个似乎就不太好。
七、关于林宁的评论
付施威的同事林宁在《你的医书是假的》下留言:
图8 林宁在《你的医书是假的》评论区的留言
以下的观点并非只针对林宁的评论,而是针对近年DDD圈子的典型反馈——“潘老师说得对,但是%……&¥&**”。
并没有恶语相向,而是潘老师说的没问题,我们也没问题,大家都Happy,这不是挺好吗?
我在《潘老师,你的气度有点小了!》阐述了一些我的看法,本文不再涉及该文中提到的内容。
本文重点展开谈一谈林宁所说的“一定程度的冗余”。
(1)问题①②③
问题①
假设只考虑领域逻辑,不考虑其他问题。我们整理包括需求规约在内的各种各样的素材,(使用面向对象方法)提炼出系统需要封装的各种领域知识,并整理成类模型。模型中,根据不同的变化频率,各种概念分别放置在类、属性、关系等合适的地方,没有冗余。
(可以理解成程序运行在一个存储空间无限大,通信和运算速度无限快且分布在全宇宙的计算机上,不用考虑什么硬盘、内存、Cache、加载,万事俱备,只欠你所分析的领域知识。)
为了方便后面的叙述,我把这称为问题①——这是最难也最想逃避的。
我依然用之前的订单例子,图9左下角的类模型,只涉及目标系统需要封装的领域概念和逻辑,不涉及任何具体实现,我们把它叫作分析模型。
(图9左下角的类模型没有行为部分。如果需要充分表达行为逻辑,可以(使用面向对象方法)建立类的状态机模型,把类的各种行为逻辑分别放在状态机的触发器、迁移动作,状态内动作等合适的地方,同样,没有冗余。)
图9 问题①②③
问题②
现实中的计算机和网络,运行空间、运行速度、通信速度都是有限制的。
如果人们对性能的要求没那么敏感,当前的计算资源又允许,那么实现模型(代码、存储……)和图9左下角的分析模型之间映射非常直接。
假设出现这样的情况:需要频繁地获得订单的总价,而频繁计算订单总价导致出现了不可调和的性能问题,解决方案之一是在实现模型中给“订单”添加一个冗余属性(用数据库语言就是冗余字段)“总价”,这就是《DDD诊所》的情况。图9右上角就是一个C#实现的“订单”类,“总价”前面有个“/”,说明这个属性是派生的(derived)。
为了方便后面的叙述,我把这称为问题②。
*可能有读者会想:这样的话,《DDD诊所》那个“订单总价=sum(订单行总价)”(前文已说过,其实应该是“总价==sum(…订单行.总价.…)”)的不变式不就有了吗?依然没有,领域逻辑并没有变化,分析模型不需要改动。这种添加冗余以及同步的套路不属于领域逻辑,实际上和具体的领域订单、物流、制造、社交……是正交的——这也是常见的批量刷废话套路,把两个正交的东西混在一起。
问题③
假设考虑到开发团队的结构,把系统分成多个“微服务”,分由各个小团队应用各自的技术栈独立完成。这时,可能会在多个“微服务”中有“订单”类。这应该就是林宁说的“拓展系统和团队”。
为了方便后面的叙述,我把这称为问题③,见图9右下角。
*注意:本文列出的问题①②③是对应《DDD诊所》的内容以及林宁的评论内容,不代表只存在这么几个问题。
(2)问题②③的出现对问题①有帮助吗?
没有帮助。
如果问题②③的出现有助于解决问题①,我们早就可以这样干了,不用等到真的有问题②③出现,假想它们存在就行了。
建模人员正在抓耳挠腮地思考和整理各种领域逻辑(问题①),太难了!
有办法。给原问题叠加一个性能问题“10亿并发用户”(问题②),再叠加一个团队问题“100个团队并行开发”(问题③),果然,问题①迎刃而解。
可能会有人提到类似下面这个:
Scott Millett等的“Patterns, Principles, and Practices of Domain-Driven Design”书中举了一个例子,一个庞大的Product类怎样被分解成若干个上下文中的小Product类:
图10 “Patterns, Principles, and Practices of Domain-Driven Design”中的例子
张逸所著的《解构领域驱动设计》中,也用了类似的例子:
图11 《解构领域驱动设计》中的例子
这看起来不就是问题③有助于问题①的解决吗?
其实这是问题①没做好。稍微有建模能力的人,谁会把这么一大堆不同变化频率的知识混到一起?
我写过一篇《发现在写代码过程中对需求的认识更清晰了》谈过类似问题,供参考。
图12 《发现在写代码过程中对需求的认识更清晰了》截图
(3)提防伪创新混水摸鱼
李三正在抓耳挠腮地思考问题①,太难了!
这时候,“创新专家”张四过来推销一种“新方法学”,大意是:环境和时代变了,你需要问题②③。
于是,李三抛开问题①不谈,先搞问题②③,工作量一下就饱满了,看着李三忙忙碌碌,李三的老板也开心。
时间过去,也许问题①没解决,但没关系,问题②③那边没有功劳也有苦劳。
时间过去,也许在忙问题②③的同时,如图12所说,懵懵懂懂,问题①也解决了一些。那就刚好证明张四的“新方法学”有助于解决问题①。
总之,问题②③的引入提供了一层遮羞布,掩盖了李三在问题①上的能力不足问题。
于是,李三觉得张四的新方法学“十分受用”。
(4)可以理解和不能忍
我拓展一下《软件方法》第1章的大楼类比:
两座大楼耸立在那里,要判断地震来了哪座大楼不容易塌,要考虑的是大楼的结构、所用的材料、所在位置的地质环境等,和这座楼是哪家公司建造的,要了多少钱,建造大楼的公司内部是怎样的组织结构,甚至大楼是猫建造的、狗建造的、外星人建造的,没有直接关系。
但要研究这些让楼不容易塌的直接影响因素,涉及到艰深的工程力学、流体力学、岩土力学等知识。架构师李三没把这些知识学扎实,正在那里犯愁呢。
这时,张四出现了。张四说,时代变了,现在盖楼要讲“新建筑学”,要考虑到人际关系,要搞好团结。
于是,李三想着反正“老的”工程力学那些我也搞不懂,还是搞“人”轻松一些。这样吧,有几个包工队跟自己混,就分几个包,大家开干就是。
转换思想后,李三每天累并快乐着,灯红酒绿,推杯换盏。
而且,运气好的时候,盖出来大楼确实也能住人。
**********
如果李三说,公司又不是我的,想那么多干什么,这可以理解;
如果李三说,这么干盖楼快,反正老板要的就是在某某大日子到来之前有个样子货交差,这也可以理解;
如果李三说,这么干有利于建筑团队的安定团结(虽然坑顾客),这也可以理解;
但如果李三和张四说,“新建筑学”盖出来的大楼更抗震,甚至到清华大学建筑学院开课“划时代革命性的工程力学”,取代原有的“工程力学”——这就不能忍了!
(5)最后说一个文风问题
短短一句话,就有“让网变成树”、“切成服务”、“拓展系统和团队”三个可以作为“创新大会”演讲标题的高大上用语。