有位同学给我发了张逸著的《解构领域驱动设计》中的一页,让我评点一下。
图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是滕云 译、张逸审的《实现领域驱动设计》中译本和原文对照,可以看到,其中把“past 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<count<size这个区间里,栈既可以压入元素,也可以弹出元素,和不能弹出元素的某个状态(可以起名叫“空”)行为规则有差异。如果我们关注行为规则差异只关注到这个程度,就认为栈处在同一状态,起名“半满”。
图28中,每个小圆圈是对象属性的一种可能取值组合。对象的当前属性值组合是其中某个小圆圈,落在某个状态中。
图28 状态是个筐,里面装了许许多多的属性值组合
以人为例,林心如、林青霞、林志玲的很多属性值是不一样的,但都可以称为“美”。
图29 林心如、林青霞、林志玲
那么,为什么我们会有“美”、“丑”这样的概念呢?它们不是凭空跳出来的,而是“血淋淋”的现实锤炼出来的——我们不断观察到“美”的人和“丑”的人适用的行为规则不一样,于是慢慢总结出“美”、“丑”的概念。
“状态”的认知来自对行为规则差异的观察。
如果不存在行为规则的差异,或者特定系统不关注这些行为规则的差异,那么相应的状态就可以当作不存在。
实际上,当前的绝大多数信息系统里面的“人”,是无法顾及“美”、“丑”状态的。例如,一个疾病预防控制系统,不会关心人的“美”、“丑”,关心的状态可能是“未感染”、“疑似感染”、“已感染无症状”、“已确诊”等。
对象之所以表现出行为规则的差异,不是无缘无故的,内因是对象属性值的差异。
我们认为林青霞美,是我们的大脑把林青霞的“身高”、“**周长”、“**周长”、“**光泽度”、“**弧度”等属性值做了个测量,把这个属性值组合放在“美”这个筐里。
如果林青霞的某个或某些属性值被改变,她就有可能会掉入“丑”这个筐里,某些行为的规则会发生变化。
而林青霞的属性值被改变,也不是无缘无故的,肯定是某些行为引起的。这些行为可能和上一句的“某些行为”相同,也可能不同。
行为规则由状态决定,状态由多个属性值的组合决定,某个或多个属性值的改变可能会导致状态改变,某个属性值可能会被多个行为改变,某个行为可能会改变多个属性值……这一切都是多对多的。这就是世界之所以复杂,或者软件(如果想封装相应的逻辑)之所以复杂的原因。
(没有外星人搞什么智能思考工具来帮助我们,人脑的速度和容量也没有大的长进,而仅仅只是炮制一些新的名词,然后再炮制一一对应的方法学,就可以让这些复杂逻辑凭空消失。这怎么可能?可惜,有不少人信这一套,还直叫“受用”,其实是自己骗自己。)
状态机想把这样的逻辑整理出来,封装在对象中,从而使对象具有“智能”。
类的公开行为(即操作)可以看作类“卖”给其他类的服务,类的“客户”们肯定希望这个部分尽量不要修改;类的属性和内部行为负责实现所“卖”的服务,这部分属于“做”的内容,可以随意抽换。
如何恰如其分地界定类应该暴露的服务,使得类能被其它类尽量使用,但又不让“做”污染“卖”,是一个难题。状态机能帮助我们靠近更合理的答案。
我们还是以人为例来说明。上面说的“美”、“丑”逻辑过于复杂(可自行搜索Facial Attractiveness,Beauty,AI等关键词),远远超出了能举例的范围,我们换一个勉强能举例的——某国法律下人的婚姻。
图30是关于人的类图,我们只列出了和婚姻相关的类和属性。从图上看,人的属性不只是姓名、出生日期这些框里的属性,还包括关联到的类,如性别、禁婚疾病、人(配偶)。
图30 关于人的类图
图31是我绘制的人在婚姻方面的状态机图。仅为示例,有的地方可能考虑不周,欢迎指正。
图31 人在婚姻方面的状态机
**********
虚拟场景:
人类高质量表妹李小k向人类高质量男性徐小根求婚,无效果(effect,状态机术语),因为徐小根虽然【无配偶】,但【未达婚龄】,从【无配偶】向【有配偶】迁移的警戒条件为假;
过了一年,李小k觉得徐小根【已达婚龄】了,又向徐小根求婚,依然无效果。李小k纳闷,不是到年龄了吗?原来,几个月前,徐小根患了MF病,健康分区的状态为【有禁婚疾病】,从【无配偶】向【有配偶】迁移的警戒条件还是为假。
又过了一年,徐小根的MF病治愈了,李小k又向他求婚,终于如愿以偿。
现居纽约法拉盛的罗大凤看李小k求婚成功,见猎心喜,买高价机票回国,隔离14+7+7天后,出关即向徐小根求婚,无效果,因为法律只允许一个配偶,徐小根和李小k结婚后,状态迁移到【配偶满】。
但徐小根和罗大凤一见钟情,难舍难分。
徐小根:“凤儿,等着我!”
罗大凤:“你打算和李小k离婚还是……”(优雅地做了一个砍头的手势-了解此句出处者可在评论区留言,第一位答对者有奖金红包)
徐小根(脸色煞白):“我是想入籍阿富汗,那边可以有四个名额,不过看见你的手势,我已经吓回去了”
(阿富汗是另一套法律,法定婚龄、配偶上限都不同,也许还有其他规则,那就更复杂了。图30和图31暂不考虑多个国家的情况。)
从以上场景可以看到:行为改变属性值,影响状态变化,导致行为规则发生变化。
**********
/*
关于图31,一些问题供大家思考:
人死亡,死前向配偶发送“丧偶”消息,“丧偶”事件触发的迁移上要加“配偶.死亡”的消息吗?
人死亡,死前向配偶发送“丧偶”消息,离婚却没有这样处理,为什么?
如果法律规定,有配偶的人,如果患禁婚疾病,必须离掉所有配偶。图上怎么改?仅是假设,目前法律无此规定。
*/
**********
有了状态和迁移之后,我们把各种原子行为片段分别放置在迁移的动作、状态的入口和出口动作等地方。当对象收到消息时,什么情况下该执行哪些行为片段,就可以根据状态机把它们拼起来。
还可以通过以下转换消除冗余的行为片段:
图32 不同的迁移,相同的动作,可以转成入口或出口动作
图33 不同的源状态,相同的迁移,可以增加复合状态
题目:
图34是某个类的状态机图。假设该类的某个对象当前在S11状态,收到消息e,会执行哪些行为片段?
图34 某个对象的状态机实例,当前处在S11状态
解析:
类图、序列图、状态机图可能有图35这样的对应关系:
图35 面向对象建模三种图的对应关系
所以这个题目相当于问图36中的这段代码是什么?
图36 类的这个操作中这一段代码是什么
1. 先判断在S11时是否响应e。
是的。
2. 看e触发的迁移有没有警戒条件,如果有,判断是否为真。
没有警戒条件。
3. 执行源状态的出口动作,如果有多层,先内后外。
离开S11,要执行z,离开S1,要执行g。
4. 执行迁移过程中的动作以及消息发送
sd
5. 改变状态
6. 执行目标状态的入口动作,如果有多层,先外后内。
进入S2,先执行z,但S2是一个复合状态,缺省迁移到S21,在迁移过程中,执行l,进入S21,执行x
所以,需要执行的行为片段依次为:z,g,sd,z,l,x。
图34的答案类似于图37:
图37 答案
更多知识,可以看我的幻灯片【剔除“伪创新”和“无领域”的领域驱动设计】。
其他一些值得一提的知识:
(1)“状态位”往往是冗余的,领域建模不需要“状态位”,需要的是状态机。
(2)如何表示自身的状态,是对象的秘密。其他对象不应直接访问对象的状态,甚至修改对象的状态,只能通过操作来给对象发消息。
(3)GoF的状态模式只是一个实现状态机的模式。掌握GoF状态模式并不能提升通过状态机建模领域逻辑的能力。
有意无意违反上面这些,就可以达到废话刷工作量的效果。
例如,每个类都加状态位,并提供getState、setState操作,刷一批工作量;
例如,每个类旁边放上一批GoF状态模式的类(至少刷3个类:1个抽象状态类+至少2个具体状态类);
……
你看,不用思考,就可以批量刷出这么多内容,爽!
(感谢 张志坚 为本文各版本所做的审校工作)
标签:状态,状态机,事件,话语,迁移,张逸,属性,全文,DDD From: https://blog.csdn.net/rolt/article/details/143119151