有位同学给我发了一页张逸的书,让我评点一下其中观点。
图1 摘自《解构领域驱动设计》(张逸,2021)
图片中的“状态和事件本质上是相同的”真是令我“耳目一新”。那就针对这页书的内容来讲讲吧。
我先讲一讲状态机的一些知识点,然后根据知识点来评价一下张逸这页书的内容。
一、状态是描述某个类的“形容词”
状态的名称和类的名称凑到一起,“状态的类”或“这个类是状态的”应该能说得通。
(也可以把整个系统当成一个类来描述状态,这时得到的状态机相当于系统的需求规约,这样的状态机往往是非常庞大的。)
例如,针对“人”这个类,描述它的形容词可以有:高、矮、胖、瘦、贫、富、美、丑……等。“高的人”、“美的人”、“这个人是高的”、“这个人是美的”是可以说得通的,这些都可以作为“人”的状态。
有的“形容词”是动词变化而来的,例如,“健身”是动词,但“正在健身的”、“已健身的”就变成了形容词,“正在健身的人”可以说得通。
我们看英文书籍中的状态机图,往往可以看到很多名称中带有“ing”、“ed”的状态,就是现在分词、过去分词作为形容词使用。“domain-driven”就属于这种情况,说domain-driven(定语) design或说this design is domain-driven(表语)是可以的。
图2是状态图。节点是状态,形容词;边是事件/动作,动词。
图2 状态图是这样的
光是这一点,不少网络上的“状态图”(statechart)、“状态机图”(state machine diagram)就已经趴下了。有的文章说“***是一张状态(机)图”,结果一看所给出的图上的节点,动词!这分明是活动图(或流程图、数据流图)嘛。
例如,下面这张图3,左上角说是描述复合状态(Compound States),但节点却是动词,这是错误的。
图3 某绘图工具给出的示例(摘自网络)
节点是动词,那是活动图,如图4。活动图的边隐含着对象(或数据)流,名词。
图4 活动图是这样的
根据以上知识点,我们来看一下张逸书页中的观点。图5是图1的一部分,我特地圈出了要评论的内容。
图5 图1的一部分,加了标注
圈出的地方,张逸的陈述如下:
①状态和事件本质是相同的,虽然UML状态图没有把状态视为事件。
②状态就是领域事件。
③领域事件的命名是动词的过去时态。
我的评论
张逸的陈述①相当于认为状态机的数学模型(如图6)中的Σ和S是一个东西,这个“创新”要是成立,整个理论体系都要推翻重来了。至于为什么张逸会有这样的认识,后面的段落还会继续深挖其中的可能原因。
图6 有限状态机的数学模型,摘自wikipedia
张逸的陈述①说的事件是UML状态机中的事件,并没有直说这个事件就是所谓的“领域事件”。也许“领域事件”和“事件”还不一样,陈述②说的“状态就是领域事件”没准就是对的呢?
说鹿是马不合适,要是我定义我这个鹿是“领域鹿”,然后说它其实就是马,也不是不可以,对吧?
那我们来看看这个“领域鹿”,不,“领域事件”是什么,然后就从本小节提到的知识点——词性来说一说张逸的陈述②和③。
“Domain Event”这个词不是DDD圈子首先用的,例如图7所示的这篇1999年的文章,就使用了domain events的说法。文中的information base用面向对象的术语来说就是“类和关系”。
图7 摘自Action Inventory for a Knowledge-Based Colloquium Agent(Erik Sandewall, 1999)
当然,DDD圈子可以自行定义这个词。以下是Martin Fowler的定义:
图8 摘自https://martinfowler.com/eaaDev/DomainEvent.html
从Fowler的陈述和所给类图可以知道,领域事件实际上就是一个“行为记录”类,像录像机一样,把发生过的事情的一些细节记下来。就是这么一个东西,没有必要过度渲染,活生生搞成玄学。
Fowler加了一个限定“affects the domain”,也就是说,不是什么都记,影响领域的才记。“影响领域”是一个模糊的说法,后面Fowler又补充得更精确一些:“can trigger a change to the state of the application(可以触发应用的状态变化)”。
以下是Greg Young的说法:
图9 摘自http://codebetter.com/gregyoung/2010/04/11/what-is-a-domain-event/
从Greg Young的说法可以得知,DDD话语中,领域事件的命名是动词的过去式,张逸的陈述③是没错的。就拿“domain-driven”来说,领域事件命名应该是“domain-drove”。
但是,动词的过去式还是动词,说的是瞬间的行为,不是形容词,不能用来做定语或表语,不能作为状态的名称。
do的过去式是did,不能作为形容词,可以作为形容词使用的是“to do”、“doing”、“done”,这也是我们常见到的状态的名称。
张逸可能混淆了“过去式”和“过去分词”(完成态),混淆了行为和状态。did是一个行为,瞬间发生就结束,done是一个状态,系统可以停留在那里很久。
规则动词的过去式和过去分词后面都是ed,也许正是这一点让张逸认为这两个ed是一回事,从而得出结论“状态就是领域事件”。碰到不规则动词,这个问题就暴露出来了。
如果把领域事件理解成“行为记录”而不是“行为”,那么这个动词其实是名词。例如,“我的奋斗”、“嫌疑人X的献身”以及“领域驱动设计”就是动词的名词化。
Fowler和Young都没有说“状态就是领域事件”。Fowler只是说领域事件触发状态的变化。
事件风暴(我重点批评的伪创新之一)的“发明”者,Alberto Brandolini在他的书中,也说:
图10 摘自 Introducing EventStorming(Alberto Brandolini,2018)
从Brandolini的陈述我们知道,他也认为领域事件用动词的过去式命名,另外他还提到“Domain Events as state transitions(领域事件作为状态迁移)”,这个说法和Fowler类似。
由此我们得知:
(1)DDD话语里面的Domain Event的命名确实是过去式。
(2)所列这几位没有说Domain Event相当于状态,最多说了相当于状态迁移。
(3)“状态和事件本质是相同的”,“状态就是领域事件”的说法可能是张逸自己加上去的。
张逸当然有资格发展出自己的东西,但最好在了解已有知识的基础上再发展,否则容易陷入“伪创新”。
张逸为什么要这样说呢?表面上的原因似乎是上面说的:
(1)他混淆了过去式和过去分词。
(2)他混淆了状态和迁移。
但问题并没有那么简单。
假如张逸退一步,不说“状态和事件本质是相同的”,“状态就是领域事件”,改口说“领域事件和状态是一一对应的,把事件的名称变换个形式就是状态了”,例如“did→done”,“broke→broken”,那可以吗?
依然是不对的!这也许就是导致张逸认为“状态和事件本质是相同的”的本质原因。
因为
二、状态和事件不是一一对应的
虽然现在分词、过去分词这样的“形容词”可以作为状态的名称,但并非状态的优选名称。
就拿人的例子来说,一个人发生了“健身”的行为,他可能有什么状态变化?
可能有的人会像图11那样,说状态为“未健身”、“已健身”:
图11 不合适的状态
“未健身”、“已健身”作为状态并非不可以,但外部的对象可能并不在意这个人是否健身,在意的可能是“美”和“丑”,如图12。
图12 更合适的状态
而要从丑到美,还有其他的迁移路线,如图13,多个事件可以触发到同一状态的迁移。
图13 事件和状态不是一一对应
或者看“技术”一点的例子,栈(Stack)。事件是压入(Push)和弹出(Pop),但我们谈论栈的状态时,显然不是像图14那样:
图14 不合适的栈状态
更合适的栈状态如图15:
图15 合适的栈状态
为突出重点,以上状态机图只保留了迁移的事件,忽略了警戒条件、动作等内容。
从图15可以看出,要迁移到“半满”状态,触发的事件可以是“压入”,也可以是“弹出”;而“压入”事件,可能会导致迁移到“半满”,也可能会导致迁移到“满”。状态和事件不是一一对应的。
再看图16的交通灯例子,状态三个,事件就一个Timer_Tick。啥,“转黄”、“转绿”等行为在哪里?藏在各个状态的入口动作中。
图16 交通灯的状态
说到这里,我们再来看看张逸的陈述。
图17 图1的部分,加了标注
张逸的陈述④解释了为什么他认为“状态和事件本质相同”,原因之一是“它们都是某个行为产生的结果,并与该行为相关联”。
我的评论
这中间的逻辑是不成立的。
炼钢既产出钢,也产生废渣。那能不能这样推论:钢和废渣都是某个行为产生的结果,所以这二者的本质相同?如图18。
图18 钢锭=废渣?
更深入地剖析背后的原因,可能是混淆了泛化和关联的区别。
我以人为例画出类图,如图19。图中人和大脑、阑尾的关联可以改为更贴切的组合型关联。
图19 泛化和关联的区别
人有男人和女人,说的是集合关系,也就是泛化关系,说男人、女人都是人,本质相同,这个可以。
人有大脑和阑尾,说的是个体关系,也就是关联关系,说大脑、阑尾都属于人,本质相同,这个就有问题了。
这可能就是张逸认为“状态和事件本质相同”,“它们都是某个行为产生的结果,并与该行为相关联”背后的原因。
我在以前写的一篇文章中就指出过滕云 译、张逸 审的《实现领域驱动设计》中译本在翻译时搞混泛化和关联的问题:猴子掰玉米?比较不同版《领域驱动设计》说“不变式”和“聚合”。
那么,怎样的表述是正确的呢?
正确的表述应该是:对象上发生某事件,可能会导致新增一个对象来记录此事件的内容,可能会引起状态变化。
我把Fowler给出的类图翻成中文,如图20:
图20 Fowler在图8给出的类图,翻译成中文版
再画一张序列图,如图21。
图21 事件发生的序列图
注意我上面陈述中的“可能”和图21中的opt。
(1)有事件发生,未必需要记录事件
电梯每天上上下下,不知发生多少次“召唤”事件,但是目前的电梯不会记录“召唤”事件的细节——谁召唤的、什么时候召唤的……,当然,也许有一天,电梯有了足够的计算和存储资源,就会记录这一切。
不记录事件,不代表事件没发生,更不代表事件没有产生效果。
(2)有事件发生,未必会引起状态变化
以图15的栈为例,假设栈的长度是1000,“空”状态下发生“压入”,迁移到“半满”,再发生“压入”,迁移到“满”的警戒条件没满足,状态并没有变化,依然停留在“半满”。
可能有人会就说,那是你的状态不合适,如果把“未健身”、“已健身”、“未压入”、“已压入”作为状态,搞一一对应,不就好了嘛?
哎,有的人就会炮制一些一一对应的“方法学”,然后兜售给需要的人。这些“方法学”的优点是简单易学,不用思考,产出巨大,是摸鱼的上佳选择。
一一对应的招数可以是:
(1)为每个属性值分配一个状态
还是以栈为例,如图22。
图22 一个属性值一个状态
如果是图22这样,那就确实满足“有事件发生,就有状态变化”了。
(2)去往各个状态的迁移对应各自的单个事件
这应该就是张逸所想象的状态机,也是许多“事件风暴”得出来的状态机(虽然他们未必画图)。如图23,去往A-ed的迁移只能由A事件触发,去往B-ed的迁移只能由B事件触发……
图23 一个状态对应一个事件
图23这样的状态机是存在的,例如“报告”的“已受理”、“已初审”、“已复审”、“已终审”。
但如果领域逻辑真的是如此简单而直接,用不用状态机来整理领域逻辑都无所谓。
而且,逻辑往往没有那么简单。一个undo事件就可以破坏这个一一对应,它可以让对象从“已复审”迁移到“已初审”,也可以让对象从“已初审”迁移到“已受理”。
那废除undo事件不行吗?只保留A、B、C,让调用者来决定什么时候A,什么时候B,什么时候C。
如果是这样,不如用下面这个更绝的一一对应:
(3)只保留“改变状态”事件
如图24,调用者通过调用“改变状态”来让对象改变状态,爱怎么改怎么改。
图24 只保留“改变状态”事件
你看,表面上我有状态机(高大上!),但又不用做太多思考,受用,爽!
但是,这样的“状态机”是没用的。
为什么状态应该是这些而不是那些,事件应该是这些而不是那些?我们就要了解下面的知识:
三、状态机到底是干什么用的
待续……
[2020.01加一套题]UMLChina建模竞赛题大全-题目全文+分卷自测(11套110题)
全程字幕-25套UML+Enterprise Architect/StarUML建模示范视频
[新增:鸵鸟]软件开发团队的脓包:皇帝的新装、口号党、鸵鸟、废话迷
《软件方法》书中自测题-题目全文+分卷自测(1-8章)16套111题
怪论:东北公司用用例做需求,反映了东北互联网落后?
别把洋垃圾当宝贝-评InfoQ中国“敏捷……”文章(一)
中文书籍中对《人月神话》的引用(完结,共110本):软件工程通史1930-2019、实用Common Lisp编程……
CTO也糊涂的常用术语:功能模块、业务架构、用户需求……[20210217更新]
UMLChina服务介绍