事件、契约设计与BDD

最近看到@gameboyLV一个讨论中提出将事件划分为嵌套事件,例如:
事件:客户提款(异步命令)
事件处理前:判断是否有足够的余额(同步命令)
事件处理: 扣款(同步命令)
事件处理后:发短信提醒客户。(异步命令)

让我联想起契约设计Design by contact,又与BDD行为驱动开发中的Given-When-Then模板类似,发这篇文章试图探究一下它们之间是否有必然的关系?

我曾经在契约设计中提出契约(合约)Contact和DDD中的聚合体同一个高度,不同点是前者侧重行为之间的关系,而聚合体是一种结构关系,我以前也曾经认为事件是一种行为关系,涉及到两个以上对象的行为是一种协调关系,可以用事件表达,而这种协调关系也是一种合约Contact,那么是否可以认为事件是合约的另外一种俗称呢?

在契约设计中,分为三种:前置条件, 后置条件和不变性。
前置条件是表达合约(事件)发生的前提条件,比如扣款这个事件或合约发生的前提是必须有足够的余额,那么前置条件是:是否有足够余额。

后置条件表达合约(事件)发生后的情况,比如扣款事件后,通知客户。

不变性是表示在事件发生后的一种状态,我们可以用值对象来表达状态的这种不变性。

当我们使用@gameboyLV 的嵌套事件用语来表达时,发现几乎是一致,而且比较通俗易懂:

事件处理前:判断是否有足够的余额(同步命令)
事件处理: 扣款(同步命令)
事件处理后:发短信提醒客户。(异步命令)
事件后的状态:聚合体内对象发生了改变,并且自此以后一直不变。

对于契约设计Design by contact表达还有使用义务和权利这样表达方式,这种表达方式更符合类似现实世界中合同的表达方式,甲乙双方签订合同,合同中规定甲乙双方的义务付出以及得到的利益分配。

其实这种义务付出是一种前置条件,得到的利益或权利是一种后置条件。
再看看契约细化定义:
1.一个契约负责管理多个实体之间的交互
2.一个契约包含权力(可以做什么)和义务(必须先做什么)
3.一个契约会辨识到在一个场景下实体之间交互的结果状态

我们如果转化为事件用语,可能会更明白简单:
1.一个事件代表多个实体之间的交互。
2.一个事件包括事件发生前后两个阶段。
3.一个事件发生后会改变实体状态。

下面一节谈谈,他们和BDD的关系。

[该贴被banq于2012-10-17 10:05修改过]
[该贴被admin于2012-10-17 10:53修改过]
[该贴被admin于2013-01-08 09:44修改过]
[该贴被admin于2013-01-08 09:44修改过]

我曾经在Eric Evans关于技术如何影响DDD的会话中谈到统一语言,所谓统一语言就是让需求人员 开发人员和测试人员都能够明白的一种统一的表达方式,类似UML,当然UML可能不合适测试人员。

@wangcity 也提出了代表其观点的统一语言,我认为非常好,而我提出的统一语言比较简单,只有三个:场景 事件和状态(Context Event and State: CES)。

这三个元素统一元素其实也表达了契约设计DBC的精神:

“一个契约会辨识到在一个场景下实体之间交互的结果状态”
用统一语言表达:
“一个事件会辨识到在一个场景下实体之间交互的结果状态

下面讨论敏捷方法中的BDD行为驱动开发模板,Given-When-Then模板也是一种统一语言,面向需求人员 开发人员和测试人员,比如以cucumber的一个案例为例子:


功能:加法
场景: 两个数相加
假如(Given)我已经在计算器里输入6
而且我已经在计算器里输入7
当(When)我按相加按钮
那么(Then)我应该在屏幕上看到的结果是13

cucumber可以将这种文本式的描述实现为Ruby Java等语言,其实Ruby 传统Java都不合适这种BDD实现,只有函数式语言或支持领域事件的框架才最合适。当然这是细节,不过多涉及,有兴趣者可以参考其主页:http://cukes.info/

这种Given-When-Then模板实质是:
假如存在什么前提条件,当发生什么事件,那么产生什么结果,导致了什么状态。

很显然,Given-When-Then模板是符合契约设计原则,假如(Given)是前置条件,Then是后置条件,When代表用户的命令出发了一种领域事件的发生,也就是契约的签订。

Given-When-Then模板也存在我提出的统一语言三要素:场景 事件和状态,其中场景可以看成是一种上下文,代表“来龙去脉”(“来龙”=前置条件,“去脉”=后置条件)


Given-When-Then模板可以推广到产品说明书的表达上,见梦工厂讨论

那么,“场景 事件和状态”这个统一语言比Given-When-Then模板或DCI数据 场景和交互或契约设计DBC的好处在哪里?

好处很简单:语境很中国化,普通中国人都能看懂都能表达。

[该贴被banq于2012-10-17 10:52修改过]
[该贴被banq于2012-10-17 11:27修改过]
[该贴被banq于2012-10-17 11:38修改过]

2012-10-17 10:43 "@banq"的内容
“一个事件会辨识到在一个场景下实体之间交互的结果状态” ...

同一场景下,实体间交互应该是同步直接发生,事件应该发生在不同场景下异步行为,实体的交互,最终表现为场景之间的交互,契约应该是高于领域层的,是应用服务的参数或返回结果。

你讲的有道理,事件有异步的特性,是不是交互应该分同步 异步两种,还是交互本身就是不讲究同步?

两个实体同属一个场景与分属不同的场景,同一场景下直接调用不产生事件,不同场景需要透过事件来相互调用,同步或着异步可能跟架构相关。
象前面提到的场景:扣款

余额检查、扣款是一个场景
发短信明显是另一个场景

我们谈论余额检查、扣款的场景的时候,
如果上述两个交互用数据库去实现的话,
其实隐含了一个数据库场景,这个场景下有CRUD这个动作,
考查数据库场景下的服务,就表现为QueryService与CommandService


[该贴被clonalman于2012-10-17 20:58修改过]

你的意思是同一场景下无需事件,直接方法同步调用即可,不同场景之间使用事件?

我原来意思,场景可能是事件发生的前置后置条件,来龙去脉。

2012-10-17 21:01 "@banq"的内容
你的意思是同一场景下无需事件,直接方法同步调用即可,不同场景之间使用事件?

我原来意思,场景可能是事件发生的前置后置条件,来龙去脉。 ...

我是这样认为的,前置后置条件只是场景的属性不能等同于场景,一个场景下除了属性之外,还有若干的服务,场景的交互就是通过这些服务来进行的,服务调用的参数可能就有前置条件或则服务返回结果修改场景后置属性等。

同一场景下无需事件,不同场景之间使用事件
[该贴被clonalman于2012-10-17 21:09修改过]

2012-10-17 20:48 "@clonalman"的内容
同一场景下直接调用不产生事件 ...

2012-10-17 20:48 "@clonalman"的内容
余额检查、扣款是一个场景 ...

为什么扣款就该直接调用余额检查?扣款的方法需要检查余额,但并不需要知道是谁检查了余额,只需要知道检查的结果就行了。

2012-10-17 21:13 "@gameboyLV"的内容
为什么扣款就该直接调用余额检查?扣款的方法需要检查余额,但并不需要知道是谁检查了余额,只需要知道检查的结果就行了。 ...

那又回到并发与数据一致性的老话题
[该贴被clonalman于2012-10-17 21:21修改过]

并不存在并发和数据一致性的问题,因为检查余额和扣款是同步事件,在“客户提款”这个大事件里完全可以先锁住用户账号,然后再操作。

2012-10-17 21:21 "@gameboyLV"的内容
并不存在并发和数据一致性的问题,因为检查余额和扣款是同步事件,在“客户提款”这个大事件里完全可以先锁住用户账号,然后再操作。 ...

如果这样,当然没问题了

检查余额和扣款这两个动作都是在一个场景的有什么矛盾?把这个两个动作封装为两个事件有点多余!

客户提款粒度比较大,可能还需要其他场景支持,如检查客户的合法性等。。。

[该贴被clonalman于2012-10-17 21:37修改过]
[该贴被clonalman于2012-10-17 21:41修改过]

如果我是双币信用卡,检查余额需要分别检查人民币和美元呢?检查人民币需要银联来处理,检查美元需要VISA来处理,如果做成事件就方便多了,以后接入欧元的业务也不用更改代码。

事件、契约设计与BDD,我们必须分清几个交互行为:
用户与系统的交互
应用层与领域层的交互
应用层与基础设施层的交互
领域层与基础设施层的交互
领域层与领域层的交互

2012-10-17 21:48 "@clonalman"的内容
域层与基础设施层的 ...

我们讨论的应该是“领域层与领域层的交互”吧
用户与系统的交互 这个AJAX或MVC都可以
应用层与领域层的交互 这个用领域服务
应用层与基础设施层的交互 这个用AOP
领域层与基础设施层的交互 这个用ORM

1)事件

描述一个事件的可能形式如下:

事件 {
属性:
参与该事件的若干个对象

方法:
1)检查事件发生的条件是否满足
2)事件发生
}

在事件中,实体是直接交互的。

2)角色

描述一个角色的可能形式如下:

角色 {
事件1,
事件(2,3),
事件4,
事件5
}

事件之间组合(composition)为一个复合事件,比如事件(2,3)。
事件聚合(aggregation)为角色(从角色身上可以知道其可以做哪些事,角色的意义在于此)。

3)场景

场景 {
属性:
参与场景的若干个角色

方法:
触发角色身上事件的发生
}

在场景中,实体是通过角色间接交互的。

4)系统

一个系统的描述,总得来说可以从两个角度进行:可见性和可控性。

“模型-行为特征(角色/事件)-场景”,以符合直觉的方式描述了系统的可控性。

事件改变模型,角色聚合事件,场景激活角色产生事件,实现模型之间的交互。

系统是动态变化的,可以根据时间段或时间点对系统的状态进行切分,切分之后,即系统可由一系列相互独立又相互联系的场景(画面)进行描述。

此外,"Given-When-Then"和"Event-Context-State",似乎与"Input-Process-Output"的描述方式并无差异?只是以黑盒的方式描述系统或场景,对于要描述系统的可见性和可控性(白盒)似乎仍不够。