有位同学给我发了张逸著的《解构领域驱动设计》中的一页,让我评点一下。
图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状态图没有把状态视为事件。
②状态就是领域事件。
③领域事件的命名是动词的过去时态。
我的评论
(1)UML状态机的状态和事件当然不是一个东西。
这一点反驳起来我都觉得不好意思。张逸的陈述①相当于认为状态机的数学模型(如图6)中的Σ和S是一个东西,这个“创新”要是成立,整个理论体系都要推翻重来了。
图6 有限状态机的数学模型,摘自wikipedia
至于为什么张逸会有这样的认识,后面的段落还会继续深挖其中的可能原因。
(2)张逸和某些DDD人士搞混了“行为记录”和“行为”。
张逸的陈述①说的事件是UML状态机中的事件,并没有直说这个事件就是所谓的“领域事件”。也许“领域事件”和“事件”还不一样,陈述②说的“状态就是领域事件”没准就是对的呢?
说鹿是马不合适,要是我定义我这个鹿是“领域鹿”,然后说它其实就是马,也不是不可以,对吧?
那我们来看看这个“领域鹿”,不,“领域事件”是什么。
“Domain Event”这个词不是DDD圈子首先用的,例如图7所示的这篇1999年的文章,就使用了domain events的说法。文中的information base用面向对象的术语来说就是“类和关系”。
图7 摘自ActionInventory for a Knowledge-Based Colloquium Agent(ErikSandewall, 1999)
当然,DDD圈子可以自行定义这个词。图8是Martin Fowler的定义:
图8 摘自https://martinfowler.com/eaaDev/DomainEvent.html
从Fowler的陈述和所给类图可以知道,领域事件实际上就是一个“行为记录”类,像录像机一样,把发生过的事情的一些细节记下来。就是这么一个东西,没有必要过度渲染,活生生搞成玄学。
Fowler加了一个限定“affects the domain”,也就是说,不是什么都记,影响领域的才记。“影响领域”是一个模糊的说法,后面Fowler又补充得更精确一些:“can trigger a change to thestate of the application(可以触发应用的状态变化)”。
我把Fowler给出的类图翻成中文,如图9:
图9 Fowler在图8给出的类图,翻译成中文版
我自己再补一张序列图,如图10。
图10 事件发生的序列图
领域事件说的是图10中的“概念B(事件记录)”,但张逸把它当成“概念A(事件)”使用,同时还把“概念A(事件)”和“概念C(状态)”搞混。
可以注意到,我在图10中给这两个行为都加了一个opt。这是因为:
*****有事件发生,未必需要记录事件(有A未必有B)
电梯每天上上下下,不知发生多少次“召唤”事件,但是目前的电梯不会记录“召唤”事件的细节——谁召唤的、什么时候召唤的……,当然,也许有一天,电梯有了足够的计算和存储资源,会记录这一切。
不记录事件,不代表事件没发生,更不代表事件没有产生效果。
*****有事件发生,未必会引起状态变化(有A未必有C)
也许对象目前的状态不响应该事件,也许可以响应该事件,但迁移的警戒条件不满足……
同样,状态不变化,不代表没有事件发生,更不代表事件没有产生效果,只是没有迁移到另一个状态而已。
**********
把“概念B(事件记录)”和“概念A(事件)”搞混的,不只是张逸,还有其他DDD人士。
图11是Greg Young的说法:
图11 摘自http://codebetter.com/gregyoung/2010/04/11/what-is-a-domain-event/
图11中,Young认为事件(说的应该是领域事件)是“something……”,说明他的定义和Fowler是一致的,领域事件是“概念B(事件记录)”,但Young又认为应该用动词的过去式给领域事件命名(这一点和张逸的陈述③一致),似乎又把它当成了“概念A(事件)”,这是矛盾的。
如果领域事件是“概念B(事件记录)”,应该用名词给它命名。
在汉语中,动词可以不做任何变化,直接作为名词使用——例如,“我的奋斗”、“嫌疑人X的献身”以及“领域驱动设计”就是动词的名词化。
在英语中,这样做是不合适的,即使用动词的过去式,那也还是动词。应该找到更合适的名词。
Young在图11中提到的“Streamlined Object Modeling”(Jill Nicola等,2002)是Peter Coad这个体系的一本著作,我们直接来看Peter Coad是怎么命名的好了,如图12:
图12 摘自“Java Modeling in Color with UML:Enterprise Components and Process”(Peter Coad等,1999)(中文译名:彩色UML建模)
事件风暴(我重点批评的伪创新之一)的“发明”者,Alberto Brandolini在他的书中说:
图13 摘自 Introducing EventStorming(Alberto Brandolini,2018)
从Brandolini的陈述可知,他也认为领域事件用动词的过去式命名,另外他还提到“Domain Events as state transitions(领域事件作为状态迁移)”。
显然也是搞混了“概念B(事件记录)”和“概念A(事件)”。触发迁移的是“概念A(事件)”不是“概念B(事件记录)”。
(3)张逸搞混了“过去式”和“过去分词”。
搞混“概念B(事件记录)”和“概念A(事件)”,Young、Brandolini和张逸都有。
搞混“概念C(状态)”和“概念A(事件)”的,说“状态和事件本质是相同的”,“状态就是领域事件”的,却是张逸独一份。
Fowler只是说领域事件可以触发状态的变化,Brandolini也只是说“领域事件作为状态迁移”。
为什么张逸会把“概念C(状态)”和“概念A(事件)”搞混呢?
原因可能是张逸搞混了“过去式”和“过去分词”(完成态),以致搞混了事件和状态。
DDD话语体系中领域事件的命名是动词的过去式(前面已经说过,这实际上是不合适的)。动词的过去式还是动词,说的是瞬间的行为,不是形容词,不能用来做定语或表语,不能作为状态的名称。
可以作为状态名称的是动词的过去分词。
英语中,规则动词的过去式和过去分词后面都是ed,也许正是这一点让张逸误认为这两个ed是一回事,从而得出结论“状态就是领域事件”。
碰到不规则动词,这个问题就暴露出来了。
do的过去式是did,不能作为形容词,可以作为形容词使用的是“to do”、“doing”、“done”,这也是我们常见到的状态的名称。
did是一个行为,瞬间发生就结束,done是一个状态,可以停留在那里很久。
图14是滕云 译、张逸审的《实现领域驱动设计》中译本和原文对照,可以看到,**其中把“paste tense”误译为“过去分词”,**说明译者以及审校者在这个知识点是混淆的。
图14 《实现领域驱动设计》原文和中译本对照 之一
**********
张逸当然有资格发展出自己的东西,但最好在了解已有知识的基础上再发展,否则很容易陷入“伪创新”。
张逸为什么要这样说呢?表面上的原因似乎是上面说的:他搞混了过去式和过去分词,搞混了状态和迁移、搞混了事件和事件记录。
但问题并没有那么简单。
假如张逸退一步,不说“状态和事件本质是相同的”,“状态就是领域事件”,改口说“领域事件和状态是一一对应的,把事件的名称变换个形式就是状态了”,例如“did→done”,“broke→broken”,那可以吗?
这也许就是导致张逸认为“状态和事件本质是相同的”的更深层原因。
但是,这依然是不对的!
因为
二、状态和事件不是一一对应的
虽然现在分词、过去分词这样的“形容词”可以作为状态的名称,但并非状态的优选名称。
就拿人的例子来说,一个人发生了“健身”的行为,他可能有什么状态变化?
可能有的人会像图15那样,说状态为“未健身”、“已健身”:
图15 这样的状态合适吗
“未健身”、“已健身”作为状态并非不可以,但人在意的往往并不是行为本身的发生,而是行为可能导致的结果,如图16是更好的表达。
图16 可能更合适的状态
从丑到美,还有其他的迁移路线,如图17,多个事件可以触发到同一状态的迁移。
图17 事件和状态不是一一对应
或者看“技术”一点的例子,栈(Stack)。事件是压入(Push)和弹出(Pop),但我们谈论栈的状态时,显然不是像图18那样:
图18 不合适的栈状态
更合适的栈状态如图19:
图19 合适的栈状态(不考虑满了追加空间的情况)
为突出重点,以上状态机图只保留了迁移的事件,忽略了警戒条件、动作等内容。
从图19可以看出,要迁移到“半满”状态,触发的事件可以是“压入”,也可以是“弹出”;而“压入”事件,可能会导致迁移到“半满”,也可能会导致迁移到“满”。状态和事件不是一一对应的。
再看图20的交通灯例子,状态三个,事件就一个Timer_Tick。(啥?“转黄”、“转绿”等行为在哪里?藏在各个状态的入口动作中。)
图20 交通灯的状态
说到这里,我们再来看看张逸的陈述。
图21 图1的部分,加了标注
张逸的陈述④解释了为什么他认为“状态和事件本质相同”,原因之一是“它们都是某个行为产生的结果,并与该行为相关联”。
我的评论
这中间的逻辑是不成立的。
炼钢既产出钢,也产生废渣。那能不能这样推论:钢和废渣都是某个行为产生的结果,所以这二者的本质相同?如图22。
图22 钢=废渣?
更深入地剖析背后的原因,可能是搞混了类之间的泛化和关联关系。
以人为例,我画出类图如图23(图中人和大脑、阑尾的关联也可以改为更贴切的组合型关联)。
图23 泛化和关联的区别
人有男人和女人,说的是集合关系,也就是泛化关系,说男人、女人都是人,本质相同,这个可以。
人有大脑和阑尾,说的是个体关系,也就是关联关系,说大脑、阑尾都属于人,本质相同,这个就有问题了。
这可能就是张逸认为“状态和事件本质相同”,“它们都是某个行为产生的结果,并与该行为相关联”背后的原因。
无独有偶,滕云 译、张逸 审的《实现领域驱动设计》中译本,在翻译时也搞混过泛化和关联,如图24。
图24 《实现领域驱动设计》原文和中译本对照 之二
关于图24的更详细说明,请见我的另一篇文章:猴子掰玉米?比较不同版《领域驱动设计》说“不变式”和“聚合”。
可能有人会就说,那是你的状态不合适,如果把“未健身”、“已健身”、“未压入”、“已压入”作为状态,搞一一对应,不就好了嘛?
哎,有的人就会炮制一些一一对应的“方法学”,然后兜售给需要的人。这些“方法学”的优点是简单易学,不用思考,产出巨大,是摸鱼的上佳选择。
一一对应的招数可以是:
(1)为每个属性值分配一个状态
还是以栈为例,如图25。
图25 一个属性值一个状态
如果是图25这样,那就确实满足“有事件发生,就有状态变化”了。
(2)去往各个状态的迁移对应各自的单个事件
这应该就是张逸所想象的状态机,也是许多“事件风暴”得出来的状态机(虽然他们未必画图)。如图26,去往A-ed的迁移只能由A事件触发,去往B-ed的迁移只能由B事件触发……
图26 一个状态对应一个事件
图26这样的状态机是存在的,例如“报告”的“已受理”、“已初审”、“已复审”、“已终审”。
如果领域逻辑真的是如此简单而直接,用不用状态机来整理领域逻辑都无所谓。
然而,逻辑往往没有那么简单。一个undo事件就可以破坏这个一一对应,它可以让对象从“已复审”迁移到“已初审”,也可以让对象从“已初审”迁移到“已受理”。
那废除undo事件不行吗?只保留A、B、C,让调用者来决定什么时候A,什么时候B,什么时候C。
如果是这样,不如用下面这个更绝的一一对应:
(3)只保留“改变状态”事件
如图27,调用者通过调用“改变状态”来让对象改变状态,爱怎么改怎么改。
图27 只保留“改变状态”事件
你看,表面上我有状态机(高大上!),又不用做太多思考,受用,爽!
但是,这样的“状态机”是没用的!
为什么状态应该是这些而不是那些,事件应该是这些而不是那些?我们就要了解下面的知识:
三、状态机到底是干什么用的
状态是对象表现出相同行为规则的属性值组合的表征。
类有属性,属性有一个可能的取值集合。
例如图19提到的栈,考虑属性count(元素个数),count可能的取值集合就是从0到size(长度)的整数。
一个类可能有很多个属性,把每个属性可能的取值集合作笛卡尔积:A×B×C……,得到的集合中的元素数目就是类的对象的所有属性可能的属性值组合的数目。
这个数目很可能非常庞大。要是乐意,我们可以把这个巨大集合中的每个元素,也就是把对象的每一个可能的属性值组合,都看作一个状态。
但这不可能也没有必要。针对某个特定系统,只要在系统关注的范围内,对象被认为表现出相同的行为规则,我们就把它看作处于同一个状态,并用一个“形容词”来归纳它。
例如,0
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?