所属文章系列:寻找尘封的银弹:自创实现模式
今天要写第一篇自创实现模式,也就是我自己独家创造的一个实现模式。
先解释一下什么是实现模式,这个词是Kent Beck《实现模式》的书名,意思就是在代码的最底层的模式,即类内部及函数内部的模式,而设计模式是多个类之间的模式。
说是独家创造,其实很可能早已有人提出来过,或者实践过,只是我不知道而已,就像设计模式的最初作者也并非是《设计模式》书中的四个作者一样,我只是一个模式的总结者而已。无论如何,对程序员有帮助,才是最重要的,也是我的初衷。
【动机】
先看一段看起来逻辑清晰,但有漏洞的代码:
void DoAdjust(ClassA **a, ClassB **b) {
if ((*a) == NULL) {
if ((*b)->next != NULL) {
*a = *b;
*b = (*b)->next;
}
}
}
注:代码中的双指针(**)只是为了在函数体内给外部的指针赋值,本质是输出参数,方式是解引用,即*a。
表面上看起来,这段代码逻辑清晰,但只是看似清晰而已,实际上编写这段代码的人并没有搞清楚这两个变量在所有执行路径到底该如何赋值。
这段代码漏掉了一种情况:当(*a) != NULL时,b也应该根据(*b)->next来决定取值,具体的取值应该有特定的业务逻辑。
当我根据需求编写了较为完备的测试用例之后,我才发现了这个Bug。经过分析,在这段代码中,确实应该有这种业务逻辑,但是这段代码却使用了默认的“不作为”行为,导致错误发生!
于是,我们加上了一些代码来补上这个漏洞:
void DoAdjust(ClassA **a, ClassB **b) {
if ((*a) == NULL) {
if ((*b)->next != NULL) {
*a = *b;
*b = (*b)->next;
}
} else {
if ((*b)->next != NULL) {
*b = (*b)->next;
}
}
}
漏洞是补上了,但再观察这段代码,总是感觉哪里不对,会不会还有漏洞?这是因为我们无法一眼看出问题,必须用白盒的方式枚举所有的情况。
由于这段代码比较简单,只有三个条件:*a、*b、(*b)->next,需要处理的情况数= 2(*a为空或非空) * 2(*b为空或非空) * 2((*b)->next为空或非空) = 8,所以枚举起来并不太难。
但是,当条件的数目猛增到10个时,它们的变化就会有2的10次方个,也就是1024个,那么枚举的方式就显得很傻,而且容易出错。不过,程序员总是想追求好方法,让自己在检查代码漏洞的时候可以更加“懒惰”一点。
有没有一种方法让这段代码看上去一目了然,一眼就能看出有没有漏洞?
【典型代码】
答案当然是有。用的方法是“以结果驱动代替条件驱动”。
先看修改后的代码,下一小节再给你解释:
void DoAdjust(ClassA **a, ClassB **b) {
//为a赋值
if ((*a) != NULL) {
*a = *a;
} else if ((*b)->next)
*a = *b;
} else {
*a = NULL;
}
//为b赋值
if ((*b)->next != NULL) {
*b = *b->next;
} else {
*b = *b;
}
}
【优劣对比】
使用该模式后的代码,看起来有点累赘,但你稍稍看仔细一点,就能完全懂得代码的所有意图和所有变化,做到了然于心。
为什么这样说?
这个函数要解决的根本问题是为*a和*b返回值,而使用该模式的代码恰恰是从这个为*a和*b赋值的角度出发的,我们一下子就能看出*a和*b是不是在退出的时候被赋过值了。这就是“以结果驱动代替条件驱动”这个实现模式中的“结果驱动”。
反观未使用该模式的代码,它是从条件的角度出发,条件包括*a、*b和(*b)->next,我们只看到了眼花缭乱的各种条件组合,却容易忽略掉最终要给每个返回变量赋值。这就是“以结果驱动代替条件驱动”这个实现模式中的“条件驱动”。
【思维进阶(一):单一路径思维、显式思维】
该模式体现出的是两种思维:单一路径思维和显式思维。
单一路径思维:函数必须确保在函数内为每个输出变量赋值的地方只有一处,而不依赖于函数内其他路径中的赋值代码。如“典型代码”小节中的代码:
//为a赋值
if ((*a) != NULL) {
*a = *a;
} else if ((*b)->next)
*a = *b;
} else {
*a = NULL;
}
显式思维:在这一处赋值的代码中,所有if/else路径都为那个输出变量赋值,而且必须包含else。代码示例也是如上这段代码。
“单一路径思维”中还提到了“依赖于函数内其他路径中的赋值”,这是反面教材,代码示例如下:
void DoAdjust(ClassA **a, ClassB **b) {
if (flag == TRUE) {
*a = NULL; //此处为“其他路径”
}
if ((*a) != NULL) {
*a = *a;
} else if ((*b)->next)
*a = *b;
} else
*a = NULL;
}
...
}
这种代码就会让人思维混乱,理不清头绪。
【思维进阶(二):多个条件的优先级】
这个模式会迫使你思考:
到底是在什么特定情况下,该给什么值。而不是遇到“条件1”先给赋一个值,再遇到“条件2”再给赋一个值,就像刚举的这个反面教材代码那样。当我们有本文这种模式的思维时,就会豁然开朗,一切逻辑都是那么清晰可见!
再看这种“营养不良”的代码产生的原因:一般是由于多个人先后修改代码产生的。后面修改代码的人不去看已有代码,他也不知道两个条件的优先级是什么,他只是加上自己的逻辑就以为万事大吉。这种情况在真实的产品代码里非常常见,它是一大类Bug的根源,却又藏得很深。
【结束语】
当我们了解了本文所讲的“以结果驱动代替条件驱动”这种实现模式后,即便不能按照这种模式修改“由堆积如山的条件”组成的产品代码,也能通过本文的思路找到Bug的根源,从而找到合适的解决方案。
作于2018-5-23
------------------------------
心定时刻:夫道不欲杂,杂则多,多则扰,扰则忧,忧而不救。
------------------------------