第3章
决策表
几十年来,人们一直使用决策表技术表达和分析复杂的逻辑关系。决策表具有严格的形式化,善于分析事件的完整性与判断事件的冗余性和一致性。此外,它还支持特定情况下的编译[CODASYL 1978]。决策表非常适合分析不同条件集情况下的行为组合。决策表也可以提供一个框架,以指导客户和开发者清晰地描述需求。表3-1描述了一些基本的决策表术语。
3.1 定义与表示法
决策表分为4个部分:粗竖线的左侧是桩,右侧是入口,粗横线的上面是条件,下面是行为。因此,我们可以将其分别称为条件桩、条件入口、行为桩、行为入口。入口部分的每一列是一条规则。如果某个行为与某个规则相对应,那就是说,在那种情况下应该采取这种行为。在表3-1所示的决策表中,如果条件c1、c2、c3都为真,那么行为a1、a2就都发生(规则1)。如果c1和c2都为真而c3为假,那么行为a1和a3发生(规则2)。
如果使用二进制条件(真/假、是/否、0/1),则决策表的条件部分就是一个旋转90°的根据命题逻辑得到的真值表。这种结构确保能够考虑到每个条件值的可能组合。在对软件的描述过程中,决策表并不是一种必不可少的表达形式,它只是一种说明性的表达。因为决策表中的条件并没有指定特殊的顺序,所以选中的行为也不会以特定的顺序发生,而且规则也可以用任何顺序来编写。
如果使用表3-1所示形式描述软件行为,我们通常会将正常情况放在前几条规则中来表示,而将异常情况放在后几条规则中来表示,这样会使决策表比较易懂。
定义:如果决策表中所有的条件都是二进制格式的,那么我们称之为有限入口决策表,表示为LEDT。
带有n个条件的有限入口决策表,它带有2n个不同的规则。
定义:如果某个决策表中所有的条件都具有可选的有限数值(>2),那么我们称之为扩展入口决策表,表示为EEDT。
定义:如果决策表中有部分条件带有可选的有限数值,其他都是严格的二进制格式,我们称之为混合入口决策表,表示为MEDT。
决策表假设所有用于评估条件的数值,对于表格的规则执行都是可用的。决策表中的行为可以改变变量值,这可能使决策表发生“?循环表格?”的行为。这就带来一个层次调用的问题:决策表中的行为可能会引用其他决策表。
3.2 技术详解
决策表严格的结构能支持某些代数运算。
3.2.1 决策表的精简
如果两个或多个规则有相同的行为入口,那么必有某个条件能在一个规则里面为真,而在另一个规则中为假。很明显,该条件在这些行为中没有起到任何作用,而这个行为会在其他规则中执行。因此表3-2可以将规则3和规则4精简,同理,也精简了规则7和规则8。
定义:如果某个条件对于由两个规则执行的行为没有影响,那么该条件的规则入口就是一个无关入口,通常使用长横线(—)标识。
规则3和规则4中,c3的入口就是一个无关入口。无关入口有两个主要的解释:该条件是无关的,或者该条件不能实施。有时候人们对后一种情况,使用“?n/a?”符号标识该条件。由于在表3-1中,同样的行为发生在规则3和规则4中,条件c3对于行为集就没有影响,所以其被无关入口的长横线所取代。同理,请见规则7和规则8。
借助布尔代数的知识可以更加理论化地解释这个问题,在一个良好形式化的决策表中规则应该是互斥的,所以第一个精简可以简单地理解为如下过程。
规则3:(c1 ^ (~c2) ^ c3)→a4
规则4:(c1 ^ (~c2) ^ (~c3))→a4
所以两条规则的互斥就是:((c1 ^ (~c2) ^ c3)→a4))⊕((c1 ^ (~c2) ^ (~c3))→a4)。
这样可以得出:
((c1 ^ (~c2)) ^ (c3⊕(~c3))→a4
由于(c3⊕(~c3))永远为真,所以(c1 ^ (~c2))→a4。
3.2.2 有互斥条件的决策表
如果条件与等价类相对应,那么决策表就具有了比较明显的特性。表3-3所示决策表中的条件是部分日历问题,它们对应的是月份变量的互斥等价类。因为它们是互斥的,所以不可能在某个规则中看到两个入口都为真。此处仍然使用无关入口长横线(—)。在这种情况下,它实际的意思是“?一定是错的?”。有些资深的决策表使用者会使用F!来强调这一点。
使用无关入口有个小问题,那就是如何确定完整的决策表。对于有限入口决策表来说,如果存在n个条件,就必须存在2n个不同的规则。如果无关入口的确表明某个条件是无关紧要的,那么我们可以通过如下方法计算规则的数目:不包含无关入口的规则视为一条规则,规则里面只要有无关入口,就将无关入口的数目乘以2加入该规则的数目。表3-2中最下面一行就是压缩规则的数目,规则数目之和为8(23)。表3-3中决策表的规则数目也显示在表格的最底下一行。注意,其规则数之和是12(理应如此)。
如果我们将这种简单的算法用在表3-3的决策表中,我们就可以获得表3-4所示的规则数目。但是,我们应该只有8(23)条规则,所以肯定什么地方出了问题。为了找到问题所在,展开这三条规则,将无关入口替换成表3-5所示的T和F。
注意,在规则1、5、9里面,所有入口都是T,在规则2、3、6、7、10和11中,其中两个条件都是真。如果将这些不可能的规则都删除,我们就只剩下3条规则。这个过程的结果如表3-6所示,其中删除了不可能的规则,使用F?!(必定为假)来强调互斥关系。
3.2.3 冗余和不一致的决策表
在识别、设计并开发一个完整的决策表过程中,需要对它的冗余和不一致性进行充分的分析。表3-7所示的决策表就是冗余的,即3个条件对应9条规则(规则9与规则4是完全一致的)。为什么会这样呢?这很可能就是由一个能力不足的设计者所实现的决策表。
注意,规则9的行为入口与规则1~4的是一致的。只要冗余规则里面的行为与决策表中对应部分是一致的,就没什么大问题。如果行为入口不同,如表3-8所示,我们就有大问题了。
如果在表3-8所示的决策表中,我们要处理一件事务,其中c1为真、c2和c3都为假,那么规则4和规则9都适用,我们可以得出以下两个结论:
1)规则4和规则9是不一致的。
2)决策表不具有确定性。
规则4和规则9是不一致的,因为行为集不同。整个表格是不确定的,因为没法判断应该使用规则4还是规则9。
3.2.4 决策表引擎
尽管决策表是一种陈述性的表达方式,但我们还是希望尽量将其结构严格化,以支持决策表引擎的执行。决策表执行引擎的输入应该是一个能够完成某个规则条件入口的所有信息,而输出动作应该是该规则对应的行为。但是这里有个问题,我们很难找到一个简单的方法来控制输出行为的顺序。不过可以使用整数入口来表示与某个规则对应的行为(而不是简单的字母形式Xs,例如a1)来表示输出动作的执行顺序。相应的,决策表引擎也可以是交互式的,用户可以针对决策表中的每一个规则提供一组数值表示。
3.3 案例分析
3.3.1 日期计算函数
因为NextDate函数的输入变量之间有很有趣的逻辑关系,所以它在软件测试领域非常有名。NextDate是一个带有3个变量的函数:年、月、日。它在执行的时候,以年月日的方式返回下一天的日期。所有数值都有边界,是正整数,年的边界是随机的。
1≤月≤12
1≤日≤31
1801≤年≤2100
将NextDate制作成一个决策表,我们需要使用精心选择的等价类作为条件,请参考[Jorgensen 2009]来获取更完整的讨论。
M1 = {月:月有30天}
M2 = {月:月有31天除了十二月}
M3 = {月:月是十二月}
M4 = {月:月是二月}
D1 = {日:1≤日≤27}
D2 = {日:日= 28}
D3 = {日:日= 29}
D4 = {日:日= 30}
D5 = {日:日= 31}
Y1 = {年:年是闰年}
Y2 = {年:年是平年}
由于这些类的笛卡儿乘积包括40个元素,因此,我们需要考虑一个带有40个规则的决策表,如表3-9和表3-10所示,其中很多规则可以被压缩。
3.3.2 汽车刮水器控制器
刮水器是由一个末端带有拨盘的控制杆控制的。控制杆有4个位置:关(OFF)、Int(间歇)、低(LOW)和高(HIGH)。拨盘有3个位置,分别为1、2、3。拨盘的位置表明3种速度。只有当控制杆位于Int的时候,拨盘的位置才起作用。下表所示为相对于控制杆和拨盘位置的刮水器速度(次/min)。
如上所述,我们几乎可以将其视为一个扩展入口决策表,只有一点区别,就是表3-11所示的刮水器速度都是独立的行为(而不是条件有多个数值的情况)。
条件c1和c2只能表明拨盘和控制杆的状态,并不能显示控制杆和拨盘的动作事件,因此这个决策表不能表示出每个状态的前置状态,也就是无法表示某种状态对前置状态的敏感情况。一般来说,如果控制杆在Int位置,其对应的规则就可以执行到拨盘位置,至少,拨盘位置此时是有意义的。第10章会更加详细地分析这个模型。
3.3.3 铁路道口门控制器
在伊利诺伊州北部,有一个铁路道口,杨树大道和芝加哥西北铁路在此交叉。在这个道口有3个不同的岔路,每个岔路都有传感器以感知火车接近道口或者离开道口。如果没有火车在道口或者接近道口,道口的门就是打开的。第一列火车过来的时候,门开始降低,当最后一列火车离开的时候,门开始抬升。如果已经有一列火车在道口内,第二列或者第三列火车已到达,那么门不执行动作,因为门此时已经是放下的状态。
表3-12是关于混合入口决策表(MEDT)极好的例子。条件c1类似内存,可以通过行为a4和a5予以更新。“不可能规则”在行为a6的入口处显示。规则1和规则3都不可能,因为如果道口没有火车,那么怎么可能有一个离开的呢?规则13和规则14也是不可能的,因为3个车道都已经被占用了。如果c2和c3都为真(规则5和规则9),那么“什么也不做”行为就可以被递增和递减的行为取代。但是“什么也不做”这个行为显示了这些输出如何互相抵消。同样,决策表不表示时间,所以对于“什么也不做”入口来说,也可能有其他理由。
3.4 基于决策表派生的测试用例
基于决策表派生测试用例是可能的,但是过程会比较麻烦。最大的问题是,决策表是陈述性的,因此不能表示其顺序。如果决策表已经被优化过,则情况会更糟糕,因为优化过程可能会改变所有或者部分条件、行为和规则。而我们也因此丧失了测试用例中输入的顺序特性,而且决策表的优化过程也可能会隐藏某些有价值的测试用例。
当然,如果设计和开发决策表的过程足够谨慎,我们还是可以获得比较完整的信息的。同样,也可以辨识出其冗余性和不确定性,相应的测试用例就能避免这些问题。然而,因为我们永远不能自动检测出缺失的条件或缺失的行为,所以我们只能获得有限的完整性。这时候就需要领域经验了,这也是基于决策表派生测试用例的过程中最好由手工完成的原因。决策表对于计算型应用来说是非常适合的,但是对于事件型驱动应用就差一点。此外,如果事件和数据的上下文与条件桩能够仔细地分开,那么我们就能比较容易地识别出上下文敏感的输入事件。在计算型和决策敏感型应用中,规则是测试用例很好的来源。在事件驱动型应用中,测试用例可能要对应一系列的规则。
3.4.1 保费计算问题的决策表
第1章中提到的保费计算问题几乎可以直接开发设计为混合入口决策表(MEDT)。年龄和“出险次数”变量已经定义了范围,可以直接引出扩展入口的条件c1和c2,如表3-13所示。优先减免是两个布尔量,因此条件c3和c4是有限入口条件。出于节省空间的考虑,表3-13分成4个部分。此处没有不可能的规则,也没有“什么也不做”的行为。拒保条件在表3-13的第四部分。
对于一个应用来说,开发出一个完整的决策表本身就有极大的好处,因为在整个过程中,我们可以确保没有任何缺失的条件。当然决策表的描述性特质在这里是一个隐含的问题。基于年龄的罚款系数应该加在因为有错被罚款之前,否则罚款金额可能不正确。如果使用决策表的绝对描述性特性来生成测试用例,那么这种事情就没法避免,此时就需要领域经验了。
在保费计算问题中,MEDT里面有39个不同的规则。每个规则都定义了一个抽象的测试用例,它们也对应第2章保费计算问题流程图中的路径。减少这些逻辑用例,生成具体测试用例,这是不能在决策表中完成的。除了一些复合条件中年龄小于16岁或大于90岁的,这39个用例与第2章流程图中派生出来的用例紧密相关。表3-14可以从表3-13的第一部分派生出来。
表3-15不能从表3-13中派生出来,但可以从表3-13中的规则里面手工开发出来。
3.4.2 车库门控系统的决策表
表3-16是针对车库门控系统的决策表模型。从表中可以看到,在车库门某个给定的上下文内应该发生的事件(条件c1的入口)。出于空间的考虑,该决策表分成两部分,第一个部分对应关车库门的操作,第二部分对应开车库门的操作。我们也可以看到上下文相关的输入事件,例如在规则1中,控制信号的响应是,向下起动驱动电机;而在规则5中,对于同样的输入事件,是停止电机。表3-16使用了F!(必定为假)标识符,显示在每个上下文(有些由不可能的规则所指示)中被禁止的事件。这里面也有个约定俗成的建模传统:禁止同时发生的事件。在这个表中,由于允许每个上下文的每个事件发生,因此我们有若干个不可能的规则。
一个简单的派生测试用例的原则是,一个规则应该对应一个测试用例。但问题是规则都很短小,只涉及一个输入事件。因此,将其理解为激励/响应对会更好些。在第4章,我们会看到这些基于激励/响应对的规则与有限状态机(针对车库门控系统)描述的事件可以很好地关联。利用表3-16派生出全部的测试用例是可能的,但只能手工完成。此时需要领域知识来减少敏感的规则序列,我们使用行为a7(循环表格)来表述这些规则。决策表的陈述特性是另一个复杂的因素,规则的选择和执行会有一些“?自然?”顺序,但这些都没有表现出来。还有一点,决策表是一次性描述。除非选择循环表格动作,否则没法显示出关门和重新开门等循环。
3.4.3 车库门控系统的测试用例
从表面上看,车库门控制器有24个规则,MEDT可以生成24个测试用例。5个“?什么也不做?”行为(规则4、12、16、20和24)都很奇怪,你怎么去测试“?什么也不做?”这样的事情呢?这些规则中的每一条都涉及光束被打断这个事件,但这个事件只有当门关上的时候(规则8)才会激活。这5个“?什么也不做?”的规则,就是应该被忽略的物理事件。(在第8章,我们会更进一步讨论如何使能和禁用光束传感器。)有10个不可能的用例(规则2、3、7、10、11、14、15、18、22和23)都是物理上不可能的。它们指的是到达轨道顶端这个事件,而这是不可能发生的。现在就剩下9个真实的测试用例(规则1、5、6、8、9、13、17、19、21)。这些行为都是合法的端口输出事件,这9个都是具体测试用例。如前所述,它们都是短小的激励/响应对。我们可以将这些基于规则的、短小的测试用例与规则序列相关联,从而使其更符合端对端的测试用例要求。表2-3中的5个测试用例在表3-17中以规则序列的方式呈现。
3.5 优势与局限
对于逻辑敏感型应用来说,决策表很明显是一种建模选择。如果能将条件表示为带有依赖关系的等价类(比如NextDate应用),那么就更适用了。同样,利用代数简化过的决策表可以让我们用更优雅的方式最小化决策表(精简决策表)。使用决策表的最大问题就是应用中的计算只能表示成行为,而且无法对行为的顺序关系进行表达。最后,输入事件必须表示为条件,输出事件表示为行为。有必要再强调一下,不能表示顺序关系这一点使得决策表这种方法对于事件驱动的应用显得不太够用。表3-18总结了决策表中行为事件的各种表达方式。
3.6 经验教训
在20世纪70年代,我是CODASYL决策表任务小组的成员,我们当时需要创建一个针对决策表的“定义性的”描述,包括推荐最终的最佳方案。最终产品由ACM公司出版,是一个加大的纸质文档的版本。在这个过程中,我们经历了几个很有趣的阶段。其中之一是,团队中有一名成员来自比利时,他提议我们应当针对当时正在实施的租赁控制法案制作一个决策表,因为这个法案非常让人困惑。在后续的一个季度会议中,我们将各自的工作成果汇聚成一个固定的决策表,搞清了这个法案中的很多事情,甚至包括一组不一致性。
另一个小组成员Lewis Reinwald有一个基于决策表的程序,它可以清除被过度修改过的Fortran程序。通常来说,这些程序是被一群既不懂原始程序也不懂太多之前修改原因的开发人员开发的。在一个测试用例中,Reinwald先生处理了一个非常庞大且维护过度的(简直就是噩梦)的Fortran程序,该程序有2000多行源代码。他从源程序中派生出一个简化的决策表,并手工“清理”了决策表,然后生成了原始程序的改进版本。改进后的版本只有800行语句。
从1978~1981年,我为一个意大利公司工作。在需求规范化过程中,我们使用有限状态机的组合,继而细化成决策表。我们发现,决策表的结构能够不断逼迫我们考虑各种可能的场景,而这些场景如果不是因为决策表的活动,那么可能一直到开发后期才被察觉。
在大学教授的课程里,我使用了已经通过州立法的关于教师退休计划奖励作为例子。在这个例子中我们发现一对会导致退休福利计算结果混乱的互相矛盾的条件。上述3个例子都说明,决策表对于理清复杂的业务规则是非常有效率的。