Date: 2014-04-19========================================================
 
最近,我在工作中发现,我们的软件产品有些bug其实是设计问题,进一步说,是需求问题。是需求导致的逻辑矛盾。只能改善,无法彻底修复。在什么情况下会有这种问题呢?主要原因在于“灵活性”的引入。某些情况下需要为软件引入灵活性,但是灵活性会破坏原有的封装。比如,重分配变更文档这个功能,它在把变更文档从源项目重分配到新的目标项目之后,能保持其数据的一致性。从这一方面来说是没有问题的。问题在于,变更文档的历史数据和项目的历史数据将不再一致。这样的bug是设计bug,或者说是需求bug。除非修改需求,否则是没有办法解决的,只能告诉用户,这是软件设计上的限制。
 
Date: 2012-09-03========================================================
 
在此我想记录我自加入SAP以后,从我经理这里学到的最重要的一个代码复查技术:设想测试用例,来复查代码。之前我一般使用的代码复查方式,无论是广度优先还是深度优先,或是分支的最后一个else情形,都是按照常规思路走的,看看代码这样写对不对,要更新的变量是否已经更新了,等等。但是,这种做法的缺点,就是可能没有设想到完整的使用场景。而设想测试用例的方法,正好弥补了这个缺点。而且设想出来的测试用例类似于一个白盒测试,代码覆盖率也是相当高的。
 
假设我们现在有一个业务函数,它用于往文件中以顺序方式记录某种类型的数据,而数据也可以被逻辑删除或物理删除。被物理删除的数据达到数据总量50%时,就重组一下数据,从一个文件复制到另一个文件并跳过那些物理删除的数据。它在执行过程中会调用一些子函数以避免它本身变得太大。要复查这样一个函数,我们可以采用以下的用例:1. 原始数据为空。2. 原始数据不为空。1.1 加入数据。1.2 逻辑删除不存在的数据。1.3 物理删除不存在的数据。2.1 加入键重复的数据。2.2 加入键不重复的数据。2.3 逻辑删除已存在的数据。2.4 逻辑删除不存在的数据。2.5 物理删除已存在的数据。2.6 物理删除不存在的数据。2.7 物理删除数据后,数据列表有足够多的空闲记录,于是重组。其中 2.x 中还可以增加四个细分支:不存在任何已删除的数据,存在已逻辑删除的数据,存在已物理删除的数据,存在已逻辑删除和已物理删除的数据。为了避免工作量爆炸,这四个细分支可以用随机方式与 2.x 组合。最后,根据这所有的测试用例,来复查代码逻辑,用思考的方式,看逻辑是否正确。
 
经过实践,发现:时间充裕的话,进行一遍变量生命周期检查,一遍全面复查,一遍测试用例复查。时间紧张的话,就跳过全面复查。切记,良好的设计是最重要的,其次才是代码的正确性。
 
Date: 2010-11-26========================================================
 
代码复查是个费时的过程,怎样做才能使代码复查的投资有回报呢?
 
假设一个程序员写代码的速度是 10KB 每天,或者说是 400 行每天。假设复查也是这个速度(可能略快些)。然后假设 bug 密度是 1 个每 25 行。假设修 bug 的代价是每个 2 天(对于大系统来说每个 2 天是完全正常的,因为程序员日子久了以后自己对代码都记忆不深刻,而好多代码特别是函数入口又缺乏注释的情况下)。那么,如果一开始不修,对于 10MB 大小,400000 行的项目来说,就是 16000 个 bug,需要 32000 人天来修复。
 
那么第一次代码复查,假设可以消除 80% 的 bug,那么花的时间是 1000 天。剩余 3200 个 bug。消除 12800 个 bug,大约节省 24600 人天的工作量。
 
第二次代码复查,假设还是可以消除 80% 的 bug,那么花的时间是 1000 天。剩余 640 个 bug。消除 2560 个 bug,大约节省 5400 人天的工作量。
 
第三次代码复查,假设还是可以消除 80% 的 bug,那么花的时间是 1000 天。剩余 128 个 bug。消除 512 个 bug,基本没有节省多少工作量,但由于客户这边 bug 数减少,因此客户生产率比等客户发现 bug 来得高。
 
接下来如果代码功能和实现都不修改,光靠复查来消除 bug,那么效率就差了,投入就不及产出了。因此,在每次复查的时候,应当记录消除的 bug 数,当消除的 bug 所带来的效益低于代码复查的投入时,就应该降低复查的频率了。或者,对于大的软件来说,当 bug 密度低于 1 个每 1000 行的时候,就可以降低代码复查的频率了。
 
代码复查要以审视的眼光来进行。对于每个代码片段都要考虑其前置条件和后置条件,对于变量、参数、返回值的和环境的读与写是否正确。由于程序员刚写好一段代码后立即进行复查时,由于比较熟悉,往往会将 bug 一起认为是正确的代码。因此为了解决这一问题,建议程序员避免复查自己刚写的代码,而复查以前自己写的代码、别人的代码则都没有问题。
 
对于新编写的功能,应该在编写完以后立即加以复查。这样可以在早期发现尽可能多的错误。
 
对于老功能的改进,应该在改进前对所有老功能的代码都加以复查。这样一来可以让老版本更加稳定,二来可以让新版本更加稳定。
 
代码复查的一个替代方案是构建单元测试并运行之。当然,单元测试本身只会被写一次,因此它的代码也要被复查,同时由于每个单元测试用例都是针对一个场景,相对比较简单,所以复查起来还是比较快的。另外,谁来“测试”单元测试呢?答案就是产品代码。比起纯粹的代码复查,单元测试的价值在于它能反复快速、自动地跑(而不像人工代码复查和测试那样需要手动执行每一个步骤),而且它能测试出一些代码复查较不易发现的问题。
 
另一个替代方案是 ad-hoc(临时设计的)测试。
 
相对于测试而言,代码复查的一个好处是它可以让开发人员熟悉代码。
 
但是,无论是代码复查、单元测试或是临时设计的测试,bug 的密度总是越来越低。由于它们总还有价值,所以可以降低做这些事情的频率,但仍渐渐在做,直到代码被大幅修改时再重新进行这些活动。
 
Date: 2009-9-22========================================================
 
代码复查的顺序:以什么顺序进行代码复查效率最高呢?
 
我的经验是:按照程序的运行流程,自顶向下,深度优先遍历代码。这样做思路最清晰,效率最高。
 
补充一点:考虑程序可能的输入(前置条件),通过等价类划分来合并类似的情况,然后在大脑里模拟运行程序,看看输出(后置情况)是否正确。
 
Date: 2005-1-3========================================================
 
对于某些程序来说,可以加进一些容错的功能,不过最好还是加进错误报告功能。错误报告可以只报告最近发生的多个错误中的一个,因为所有这些错误都将导致程序不可用。另外,函数可以对参数的合法性进行检查,但是这不是最重要的,而且为了编程的方便与易读,这个检查其实没有必要一直都做。但是对于用户提供的数据应该作检查。这是保证程序稳定可靠的关键。还有一点:如果是一个类的成员函数,在操作前应该检查对象的状态。
 
Date: 2004-2-29=======================================================
 
之前我提到过要对所有要同步的数据做一个列表,以保证数据一致性。这里我要说的是我们要对所有的匈牙利命名法中的变量前缀也做一个列表以说明每个前缀分别是什么意思。
 
Date: 2003-9-30=======================================================
 
经过最近三次程序开发的经历:HTTP SERVER、图形学程序、为陈中润写的本地内存分配程序,我发现 CODE REVIEW(可以叫做代码复习)是一个很重要的 DEBUG 的方法。一般来说,程序每到一个关键时候,就应该做一下 CODE REVIEW。但是 CODE REVIEW 比较花时间,不可以经常做。
 
同时 MODELING 也是很重要的一个技巧,因为出于某些程序的特点,它可能不是一个简单的模型。比如说我的 RPSHELL,它在打开文件的时候最好是使用一个第三方代理“OPEN AGENT”。为什么呢?因为打开一个文件需要好几个步骤,所以我们现在要做这件事,就要使用一个 AGENT,让它和 SHELL 进行交互。具体的实现我还没有做,但是我想象起来就是把它封装在一个函数里,然后在其他函数里面注册这个 AGENT 所需要的消息。当这些消息发出时,让这个 AGENT 函数来处理这些消息。这样一个打开文件的过程就可以完成了。
 
Date: 2003-4-27=======================================================
 
今天我发现了发现程序错误的一个很好的办法:一边写注释,一边检查。另外,根据微软公司的工程师林斌的说法,一边给别人讲解自己程序一边自己也能发现很多错误。
 
Date: 2002-10-19======================================================
 
变量放在哪里定义?1. 如果变量只在这一个函数里面有用,那么只在这一个函数里面定义。2. 如果变量对好几个函数都有用,那么应该在调用这个函数的调用者那里转给这些函数。避免使用全局或静态变量。3. 如果变量只是在一个分支或者循环里面有用,那么用大括号把变量括起来。4. 如果变量只是在一个函数的一段代码里面起作用,有两种方法来处理:把变量用大括号括起来,或者把函数拆分成几个函数。
 
Date: 2002-6-9========================================================
 
Robbie Mosaic 的经典思想:一个好的程序像一幢好的房子,它外观美丽,内部结构四通八达,但是不会漏风漏雨。而且,每个部位都容易清扫。
 
Date: 2002-5-25=======================================================
 
对于分支语句,把所有的条件都考虑一遍(比如有 if 一定也要考虑 else)。
 
Date: 2002-4-1========================================================
 
不会用好 Ctrl + C 和 Ctrl + V 的程序员不是好的程序员。同样,不会用好查找和替换的程序员也不是好程序员。
 
Date: 2002-4-1========================================================
 
判断一个程序正确与否,就是看它在理想的环境下能否保持其数据结构的完整性,以及能否实现期望的变化。
 
Date: 2002-3-9========================================================
 
今天,我发现一个事实:编写程序或进行设计时,最好先从大局来观看事物,这样,错误很容易被发现,不必要的事也可以不做。当然,细节也很能影响大局,所以必须在适当的时候改变整体策略。虽然这样做可能需要较大量的代码修改以及重写,但是非常有助于程序的逻辑严密性和实用性。
 
Date: 2002-2-19=======================================================
 
在编写程序时,一般不要过早追求程序功能多,因为在没有完全掌握程序特性之前,很有可能作出错误的估计,写出低效或错误(令人感到不爽)的代码。应该逐步建立整个程序。
 
对于有些低质量的代码,宁可重新写,也不要不舍得丢弃。
 
Date: 2001-12-9=======================================================
 
我发现了一个实用的检查程序错误的方法:
 
每个函数单独检查的时候,跟踪函数中的每一个变量,如果对每一个变量的操作没有问题,那么函数也应该没有大问题,最多有一些小问题。
 
Date: 2001-10-4=======================================================
 
1. 我建立了一种通用的命名变量的方法:用不多于三个的小写字母表示变量的类型,作为前缀。然后紧跟一个下划线,再用类似 Java 的变量命名方法(第一个单词小写,以后的单词首字母大写)来进行余下的命名。这一命名方法还是麻烦了一些,只适用于全局的变量或对象。
 
2. 同样地,我建立了一种通用的函数名称命名方法。首字母是否大写并不重要,关键是如果用一个动词加一个名词构成函数名,那么就应该把动词写在前面。
 
3. 我感到,最人性化的命名方法就是骆驼命名法(Java 所采用的),而最人性化的代码书写方法是 QuickC 的帮助及例子中的代码书写格式。
 
Date: 2001-9-16=======================================================
 
编程时需要注意的一点:就算让循环变量多一点,也不要为了节省循环变量而用一个循环变量代替好几个。如果这样做,不仅会使得程序的维护工作变得更困难,还会降低可读性,效率其实也没有什么大的提高。
 
例子:
char chars1[80], chars2[160];
int i, j;
for( i = 0; i < 80; i++ )
{
    chars1[i] = chars2[i + 80];
}
for( i = 0, j = 0; i < 80; i++, j++ )
{
    chars1[i] = chars2[j];
}
 
Date: 2001-9-7========================================================
 
编程时逻辑结构清晰的保障,也是避免编程错误的一法:每写一步,把新建的函数与数据结构之间的关系写下来。对于某种特定的算法,应该先写下来,再进行编程。
 
Date: 2001-8-8========================================================
 
1. 在一个 for 循环中,有一个固定的结构模式:
 
循环变量初始化
 
循环条件检查
 
主要处理部分
 
循环变量递增
 
只要遵循以上的规则,就不会犯错误。
 
Date: 2001-7-27=======================================================
 
1. 编程序以后,应该对程序仔细地加以观察,尽可能凭借眼睛来找到大多数的错误。
 
2. 对于一些比较难以处理边界问题的程序,比如将一个 unsigned char 数组中的每一个成员都向右移动一定位数,高位空档由高位字节的内容填充,那么这个程序必须考虑头上一个字节和末尾一个字节的问题。这样一来,边界问题就变得很复杂了,为了避免复杂的边界问题,我采用了一种办法,既然移动的位数不超过八位,那么就可以在原数组的头上和末尾补上两个字节,内容都是零,这样,问题就轻而易举地解决了。具体情况请参见 HUFFMAN 压缩的 COMPRESS.C 程序。
 
Date: 2001-7-19=======================================================
 
1. 在 C 语言中,很有必要注意整数循环的初始化,一般从 0 开始。HUFFMAN.C 的编写中我曾经就犯了这样一个错误。
2. 类似地,在 C 语言中,(struct Type *)malloc(sizeof(struct Type *)) 是初学者经常犯的错误。正确的应该是:(struct Type *)malloc(sizeof(struct Type))。
 
Date: 2001-7-18=======================================================
 
1. 对于需要释放的动态分配内存,在写好申请分配的代码以后,紧跟着写上释放的代码。
 
2. C 语言中最要小心的是整数和字符的不同类型。要注意它们的长度、正负的可能性、使用场合。
 
Date: 2001-6-23=======================================================
 
我发现了三个可以用于防止编程时出现错误的方法:
 
1. 对于功能上可以分离,但篇幅较小,并且适合放在一起的代码,可以放在同一个函数中。要点是用大括号把代码括起来(利用 C 语言的特性),以免变量名称的冲突。当然,这并不是必须的。视情况而定。
 
2. 对于需要保持数据一致性的部分,加一个注释,把所有需要保持一致性的内容写在列表中。
 
3. 在使用长变量名时,建立适当的规则,并将这些规则写入注释,同时,在写代码时,不时地检查拼写。
 

返回“编程天地”