首页  编辑  

調整心態

Tags: /超级猛料/Book.凤毛麟角(电子书籍片断)/完美程式設計指南/   Date Created:
調整心態
本书从头到尾,我都在谈你能用来侦测跟避免错误的技巧。使用这些技巧不保证你就能够写出零错误程序,就好象一队有熟练的球员也不能保证你赢得球赛。其它必要的关键包括良好习惯与态度的组合。
如果球员们都在抱怨自己为何要练习,你认为这些球员会赢得季赛冠军吗?如果他们经常为了年薪只有一百二十万美元或总是在担心被出卖或炒鱿鱼,他们的表现会如何?这些都跟球技无关,可是跟球员的表现息息相关。
你可以用本书中全部的建议来帮自己消除错误,可是如果你的态度或程序写作习惯上就有问题,你就很难写得出零错误程序来。
在本章中,我将谈到一些写出零错误程序的最常见障碍。这些障碍都很好修正;往往,你需要的只是注意到这些障碍的存在而已。
我的下个把戏可以把错误变不见
你多常在问别人修好错误时听到他们回答, " 哦,那错误不见了吧 " ?我许多年前对我的第一个经理说过同样的话。他问我,找到一个我们正在开发的苹果二号计算机数据库产品中的一个错误找到没,我说, " 哦,那错误不见了。 " 那经理楞了一下,然后把我叫进他的办公室内,我们都坐了下来。
"Steve ,你说'错误不见了',那是什么意思? "
" 喔,你知道嘛,我照着错误报告中的步骤检查过,可是那错误没出现啊。 "
我的经理靠回他的椅背。 " 那你认为这问题怎么了? "
" 我不知道 " ,我说。 " 我猜问题已经被修好了吧。 "
" 可是你不知道问题有没有被修好啊,不是吗? "
" 我是不知道,我想我不知道 " ,我必须承认这点。
" 那你不觉得你最好把真正发生了的什么问题找出来吗?毕竟,是你在用计算机工作,而不是计算机在帮你工作;计算机不会自己把问题修好的。 "
那名经理继续解释了三种错误消失的原因:错误报告不正确,别的程序员修好了问题,或者问题还在,只是问题不明显。他的最后一席话提醒了我,作为一名专业程序写作者,我应该决定出我碰到的那些不见了的问题是哪一种情形,并得据此处理状况。我不应该因为一个问题消失了,就忽略掉它的存在。
在我第一次听到那番建议的 CP/M 跟苹果二号时代里,那是很宝贵的意见。其实那番建议在之前几十年里就很珍贵了,即使到了现在,那一席话对我来说,还是无价之宝。一直到我成为一名项目主持人,我才了解那番话有多么珍贵。我发现对程序员们来说,他们常会假设测试人员搞错了,或者已经有人把问题修好了。
程序中的问题常常只因为你跟测试人员使用不同版本的程序而消失。如果一个错误在你使用的程序中没出现,你该换用测试人员用的版本看看。如果问题还是没重现,通知你的测试人员重新检查一下。如果错误又出来了,往更早期的版本中找找看,决定该怎么修理这个问题,然后看看为什么问题在现在的版本中会消失。更通常的,一个错误还在,只是被周围的改变给遮盖住了。你得了解为什么问题消失了,你才能采取适当步骤来修正它。
臭虫不会自己 " 消失 " 。
太多努力?
当我要求程序员们把旧版的原始码拿出来检查一个已知的问题时,程序员们时而会抱怨;他们抱怨说这样子似乎是在浪费时间。如果你也这么认为,考虑一下,我并不是因为一时兴起才叫你把老程序代码找出来的。叫你把老程序翻出来看是因为问题很可能也出现在旧版的原始码中,而且在旧版本的程序中找起错误来比现在的版本更有效率。
假使你在较早版本的原始码中找到了问题所在,并发现那问题已经确实在目前的程序中修正了。你浪费时间了吗?没有吧。毕竟,将一个消失的问题标示为 " 修好了 " 或是 " 无法重现 " 后再送回去叫测试人员把它找出来,与前面的做法比较起来,哪一种比较好?测试人员当然不能假设问题修好了-他们只有两个选择,花更多时间试着把问题重现,或是跟你作的一样,当作问题是不能重现的,希望问题真的已经修好了。两种选择都比你自己在较早期的原始码中找出问题来,把问题真正标示为 " 修好了 " 来得糟糕。
实时修正问题才能避免更多问题
当我第一次进到 Microsoft Excel 的小组时,人们的做法是把所有错误的修正都放到项目末尾再处理。并没有一块写着 " 你在所有功能都写好以前不应该修理任何问题 " 的铁板钉在小组工作室的墙上,可是我们有赶上时间表跟作出程序功能的压力。同时,很少压力要求我们把问题处理掉。有一次我听到说, " 除非臭虫把系统当掉了或者困住了测试小组,不然不要管修正错误的事情。反正在我们完成所有预定功能后,我们还有很多时间可以修正问题。 " 简单说来,修正错误并不是优先该做的事。
我确定这对现在的微软程序员们听来有些退步,因为没有任何项目再以那样的方式进行了;那样的做法有太多问题,而且最糟糕的是难以预料产品哪时会完成。你怎么估计修理 1742 种错误需要的时间?当然,不会只有 1742 种问题等着你-程序员们在修理老问题时还会产生新问题出来。而且(相关的)错误修正可能引出其它测试小组在一开始因为先前的错误而没找到的潜在问题。
这些还不只是唯一出现的问题而已。
先完成程序功能再修正错误,程序员们得让产品开发时间看来比真正需要的还短。公司的重要人物会用内部版本,来产品是不是除了偶尔出现的问题以外都能够正常动作,然后怀疑为何开发人员得花六个月来完成一个接近完成的产品。他们不会发现内存不足的问题,也不会发现他们没用到的功能中出现的错误。他们只知道程序的功能完成了,而且基本上会动了。
在项目开发周期末尾花好几个月来修正问题在团队士气上也是一大打击。程序员们喜欢写程序,而不是修正问题,可是在开发每个产品的末尾,他们得花好几个月,就只是在修理问题,承受沉重的压力,只因为开发部门以外的人都认为产品接近完成了。为何产品赶不上 COMDEX , MacWorld Expo 这些世界级的展览,或一个地区计算机俱乐部的聚会?
真够糟糕的。
一系列错误百出的产品,从麦金塔版的 Excel 1.03 开始,到一个取消推出-因为到处横行的臭虫清单太长了而取消-的未公开 Windows 版产品,强迫微软公司对开发了的产品采取严密的检查。然后得到底下不太令人惊讶的发现:
�         在产品周期中延后修正错误并不会省下时间。事实上,因为修理一个出现一年多的错误比修正一个出现没几天的问题经常更难,你会失去更多时间。
�         立即修理问题控制了错误造成的伤害,因为你预早了解怎么犯下错误的,你愈不可能重蹈覆辙。
�         臭虫是一种抑制速成的懒散程序员出现的一种负回馈现象。如果你不让程序员在修理好所有自己产生的问题前继续写新功能,你就能避免懒散的程序员在程序中到处留下写了一半的功能-他们只会忙着修正错误。如果你让程序员忽略他们的错误,你就丧失了团队纪律。
�         让错误数维持在零附近,你就更容易预测何时能完成产品。不用再猜要花多久才能完成 32 项功能跟 1742 种错误修正,你只需要推算 32 种功能要花多久完成。更棒的,你随时都可以视需要放弃未完成的功能而推出产品。
�         这些发现不只适用于微软的程序开发;他们对任何软件开发团队也同样适用。如果你在发现问题时没有立刻修好它们,把微软公司的负面经验当成你的教训吧。你当然可以自己从错误中学习,不过从别人宝贵的失败经验中获取教训不是更好吗?
不要以后再修理错误;现在就把问题修好吧。
臭虫救星来了!
在 Awaken the Giant Within 一书中, Anthony Robbins 说了一个医生的故事。一名女医生在湍急的河流边听到有人掉进河里的惨叫,她看看四周都没人能帮忙,就自己跳进河里去救人了。当她游过去把人就到岸边,并作了口对口人工呼吸后,那人还是没有呼吸,而且她又听到河里传来两声惊叫。她又跳下去救起那两个人,而当她在抢救那两个人时,她又听到四个人叫着救命,然后她听到八个人叫着救命 .... 不幸的,这医生忙着救人,根本没有时间管到底谁把这些人全丢进河里。
就像这名医生,程序员们有时忙着 " 治疗 " 错误而从来没停下来找出到底什么原因造成了错误。我们在第七章中碰到的 strFromUns 函式就是这种问题的一个例子。 strFromUns 产生问题是因为它强迫程序员们在静态内存中传递资料。不过当错误发生时,它们都出现在 strFromUns 呼叫链的下游,而非 strFromUns 本身。你觉得哪个有问题的函式会被修好?是 strFromUns -问题的真正根源-或是呼叫 strFromUns 把前一个呼叫的结果清掉的那个函式?
同个问题的另一个例子发生在我将一个 Windows 版 Excel 的功能移植到麦金塔版上时。(当时这两个版本还是使用分开的原始码。)在我把功能移植过去后,我开始测试程序,并发现一个函式碰到一个意料之外的 NULL 指针。我检查了程序,却不清楚问题是在呼叫者(传入了个 NULL )或在函式中(没处理 NULL )。我去找本来的程序员,并解释出问题的状况给他听,他把那段程序加载编辑器后说, " 哦,这函式不能接受 NULL 指针。 " 然后,我站在那边看着他加上一个在 NULL 指针出现时立刻离开那函式的修正处理:
if (pb == NULL)
   return (FALSE);
我说不应该传入一个 NULL 指针给这函式,问题应该是在呼叫者,而不是在这函式中,可是他说, " 我了解这程序;这样就能修好了。 " 问题是修好了没错。可是对我来说,这样的解决方式就好象我们只治好一种疾病的症状,并没有除去病因。我回到办公室后,花了十分钟把原始码里所有的 NULL 指针都找了出来。真正的问题不光是 NULL 指针,还有两个其它已知的错误。
我追踪到了其它两个错误的根源,然后想着, " 等等,这样写不对;如果是这样,这边的函式也会坏掉,所以这地方不应该是这样写的。 " 我确定你能猜出为什么别的函式会有效运作,因为有人对一个广泛的问题作了区域性的修理。
治疗病因而非症状。
你是个爱管闲事的程序员吗 ?
" 没坏的东西也该修理几下 " 似乎是某些程序员过度拼命的写照。不管一份程序动作得多少,某些程序员都觉得应该在上头改些什么东西。如果你曾经跟一名会将整个程序重新整理成他或她喜欢的格式的程序员共事过,你就知道我在说什么了。大部分程序员的保守程度得称不上那种 " 习惯改东改西 " 程度的人,不过所有程序员似乎都会将程序某种程度上都会把程序改来改去。
清理程序代码的麻烦在于程序员们不种是将他们改进过的程序版本当作新的程序代码处理。有程序员会在检视整个档案时看到底下的程序而想把那个对 0 的测试改成对 '\0' 的比较;其它人会想把那个测试一起拿掉。
char *strcpy(char *pchTo, char *pchFrom)
{
   char *pchStart = pchTo;
   while ((*pchTo++ = *pchFrom++) != 0)
       {}
           
   return (pchStart);
}
把 0 改成零字符的问题在于这样从一打成 '0' 而不是 '\0' ,可是有多少程序员会在这样简单的修改之后去测试 strcpy ?有个更好的疑问:当你改了如此简单的东西后,你会把程序当作重新写过般的彻底测试吗?如果你不会,你就冒着经由这些不必要的修改产生出错误的风险。
你会认为有些修改不可能会出错,因为程序还是能编译过关啊。举例来说,改个区域变量的名称怎么可能产生错误?喔,当然可能。我曾经追踪一个问题到一个函式中,里头有个叫做 hPrint 的区域变量,跟一个同名的整体变量互相冲突。因为这函式一直到最近都动作得好好的,我看了一下旧版的原始码,想找出哪里改了什么东西,好确定我的修正方式不会重新产生旧问题。结果我发现整个程序都被清理过了。早期版本中有个叫做 hPrint1 的区域变量,可是没有 hPrint2 或 hPrint3 ,让那个 '1' 看来似乎不是必要的。不知道谁把那个 '1' 拿掉了,这个人假设 hPrint1 是以前留下的东西,就把它整理了一下,造成名称上的冲突跟程序执行错误。
避免让你自己产生同样的清理程序错误,在屏幕上贴上一个小讯息:跟我共事的程序员们不是笨蛋。在你以为程序明显写错或没必要时,这样的讯息应该会提醒你要特别小心。如果程序明显可疑,可能有个不明显的好理由在里头。我看过只为了绕过编译器产生执行码错误的问题而写得很滑稽的程序,把那段程序重新清理过就会产生出老问题来。当然,这样的程序应该会有个批注说明为什么这样做,不过并不是所有程序员都会想到那么多。
如果你看到像这样的程序
char chGetNext(void)
{
   int ch;       /* ch必须是个整数。 */
   
   ch = getchar();
   return (chRemapChar(ch));
}
不要把那个明显 " 不必要 " 的 ch 拿掉:
char chGetNext(void)
{
   return (chRemapChar(getchar()));
}
如果你拿掉 ch ,你会在 chRemapChar 是个将参数评估超过一次的宏时制造出问题来。保留那个 " 不必要 " 的区域变量,就能避免掉不必要的错误。
除非对程序的更动对产品的成功很重要,不然不要这么做。
将酷毙了的功能冷冻起来吧
避免把程序代码改来改去只是一条更广泛避免错误的原则的特例:不必要时不要写程序(或改程序)。这似乎是个奇怪的建议,不过你一定会被自己多常问自己 " 这功能对产品的成功有多重要? " 而吓到。
有些功能对产品毫无价值,存在只是为了填补门面;其它功能存在则是因为大公司的顾客要求加上这些东西;剩下的则是因为竞争产品里头也有类似功能,而某杂志的编辑决定把这些功能放在功能列表上。如果你有良好的行销语产品规划队伍,你就不应该再加上这些用不着的功能。不过作为一名程序员,你会碰到这些不必要的功能,或甚至你自己就是这些不必要功能的来源。
你曾经听过程序员说过类似 " 如果 WordSmasher 可以怎样怎样,那真够酷的了 ..." 的话吗?问题是,一个功能很炫是因为它能改进产品的特性,还是因为把它作出来在技术上是一种挑战?如果一个功能可以改善产品表现,那等到你程序的下一版再来实作这东西的话,不是更能对这功能作出适当的评估跟规划吗?如果写出一个功能只具有技术上的挑战性,拿掉那功能吧。我并不是在扼杀创意;而是在建议避免不必要的功能跟相关错误的出现。
有时具有技术挑战性的功能是可以改善产品表现;有时则不然。小心选择吧。
不要实作不具策略价值的功能。
天下没有白吃的午餐
" 附加 " 功能是不必要问题的另一个根源。表面上,附加功能似乎值得,因为它们以现成的设计方式几乎不费代价就可以完成。有什么比不用代价的东西更好的?不过附加功能有个大问题存在-对产品的成功与否几乎不重要。当然,不重要的功能是错误的潜在根源。程序员们在程序中加上附加功能,因为他们可以加上那些东西,而不是因为他们应该加上那些东西。毕竟,不加白不加,又不费事,对吧?
这真是荒谬的想法。附加功能也许不费太多事,可是比起在程序中加上了一个功能,得有人替那功能写出使用文件,得有人测试那功能,而且当然也得有人修理所有跟那功能相关的问题。
当我听到一名程序员说一个功能是附加的时,那告诉我说他或她一定没花多少时间想过加上那功能的真正代价有多高。
世界上没有不用代价就能加到程序中的功能。
弹性带来问题
另一个能让你避免错误的策略是从设计中去掉不必要的弹性。从第一章起,你就看我用着这条准则。在第一章中,我用了选择性的编译器警告功能来禁止多余而危险的 C 语言语法的使用。在第二章中,我把 ASSERT 定义成一个用来防止这宏被误用在表示式中的叙述 . 第三章中,即使我可以用 NULL 参数呼叫 free 函式,我还是用了个除错检查来捕捉传给 FreeMemory 的 NULL 指针。在每一章中,我都列出了一些我去除弹性来防范错误的例子。
弹性设计的麻烦在于,愈有弹性,愈难找出里头的问题。记得我在第五章中讨论过关于 realloc 的部分吗?你几乎可以把任意组合的输入参数丢给 realloc ,而它一定会完成某些动作,即使这些动作不是你预期要作的。更糟糕的, realloc 的问题很难找出来,因为这函式弹性到了你没办法加上有意义的除错检查来核对输入参数。不过如果你将 realloc 切成处理扩大、缩小、配置跟释放内存块的四个分开的函式,你核对起函式的参数就简单多了。
除了注意不当的弹性函式,你也应该留心那些弹性过度的功能。弹性功能麻烦多多的原因在于它们能带来意料之外的合法状态,让你没想到要测试这些你甚至不知道会出现的状态。
当我在替苹果计算机公司的第二代麦金塔计算机加上 Microsoft Excel 的彩色显示支持时,我将 Windows 版 Excel 里头允许使用者指定电子表格储存格中文字颜色的程序移植过去。要在一个储存格中加上颜色,使用者得有个现成的储存格显示格式如下
$#,##0.00          /* 将1234.5678显示成$1,234.57。 */
然后在这格式字符串前面加个颜色指定代码。要把一个数字显示成蓝色,使用者会把上头的格式改成
[blue]$#,##0.00
如果使用者用了 [red] ,显示出来的数字就是红色的。
Excel 的产品规格相当清楚-颜色指定代码应该放在数字格式字符串之前-可是在我把这功能移植过去,并开始测试程序后,我发现底下的格式也有效:
$#,##0.00[blue]
$#,##[blue]0.00
$[blue]#,##0.00
使用者可能会将 [blue] 放在任何地方。我问本来的程序员,这算是个错误还是个程序特性,他说颜色指定字能摆在任意处 " 只是刚好在语法分析循环中排定的结果。 " 他看不出来允许这样一点的额外弹性会有什么问题-我当时也没看出来-所以程序就保留那样没改。回想起来,我觉得我们不应该让那点额外的弹性留着。
不久后,测试小组找到半打隐藏的错误,全都跟格式语法分析部分有关,语法分析程序并没有预期会在一个格式字符串的中间碰到一个颜色指定字。
不幸的,我们没有拿掉这点额外弹性来修正问题-一个只要修改一个简单的 if 叙述就好了的改法-另一个程序员跟我一直在修理那个指定字的错误-修理症状-来维持没人需要的弹性。到今天, Microsoft Excel 还是让你能将颜色指定字放在任何地方。
当你在自己的项目中实作功能时,要让它们易于使用;不要给它们不必要的弹性。易用跟弹性有很大的差别的。
不要允许不必要的弹性。
别处移植过来的程序都算新的
我从移植这么多 Windows 版 Excel 的程序代码到麦金塔版 Excel 的过程中学到的一个教训就是,人们会想跳过测试这样移植过的程序代码。你的理由一定是 " 我已经在本来的产品中测试过那程序代码了 " 。我应该已经在测试小组看到程序以前就把 Excel 数字格式程序中的所有问题都找出来了,可是我没有。我只有把程序放进麦金塔版 Excel 中,修改了把程序整合进项目中所必要的部分,然后简单的测试一下正确整合好了没有。我没有彻底测试这功能,因为我想我已经测试过了。这是不对的,特别在 Widnows 版 Excel 本身还在开发中,而微软内部各开发小组还把修正错误的时间排在产品开发周期最后头的时代里。
不管你怎么实作功能的-不管是从头写起或是从现有的程序改起-你都有责任让错误在你把一段程序加到项目内时全抓出来。事实是 Windows 版 Excel 上的同样问题不见得比麦金塔版 Excel 的轻微。我的懒散都在程序中表现出来了。
不要乱试
你有多少次听到像这样的对话, " 我想不出来该怎么 ..." ,然后另一名程序员回答, " 你试过 ..." ?这样的对话总是以不同的形式出现在几乎每个程序设计的网际网络新闻群组中。一名程序员会贴出一段讯息,问 " 我怎样把光标隐藏起来,好让它不会遮住画面? " 而有人会答, " 试着把光标移到画面外的坐标。 " 另一个人则会建议, " 试着将光标屏蔽设成 0 ,表示没有半个光标上的图素会被显示出来。 " 还有第三个人可能会说, " 光标只是位图形而已,把它的宽度跟高度设成 0 就好了。 "
尝试,尝试,尝试。
我承认这是个蠢例子,不过我确定你一定看过这类的对话发生过,不管是在网际网络新闻群组或是在办公室的咖啡机前面,而这些别人要你尝试的做法里头只有少数找对了方向。当有人要你试着怎样解决一个问题时,他们是在给你一个经验上的猜测,而非正式的明文解答。
试着用不同方法解决一个问题有什么不对?没有不对,只要你尝试的每件事都在你使用的系统上清楚的定义着。不过程序员们在尝试这些东西时,往往表示他们已经来到超出了已知系统领域,而进到了找寻任何会动的解决方案的领域,即使这些做法可能依赖于一个无意间造成而未来可能改变的副作用。你认为那些故意从已释放内存中读取东西的程序员们是怎样养成坏习惯的? free 当然没有定义它会对已释放的内存内容进行什么处理,不过有时那些程序员会觉得他们需要参考到已释放的内存。他们这样试了,程序动了,所以他们现在认为这样的行为是可以依赖的。
注意听那些 " 你试过 ..." 后头的建议,我想你大概会发现大部分的建议都用到了未定义或定义不良的程序副作用。如果作出这些建议的程序员们知道正确的解决方法,他们就不会告诉你 " 试着 " 怎样作了,他们会告诉你, " 只要用系统呼叫' SetCursorState(INVISIBLE) '就好了。 "
不要一直 " 尝试 " 不同的解决方案来找出有效的做法。
花时间找寻正确的解决方法才是正途。
不要 " 乱试 " ,看看文件吧
有好几年,微软的麦金塔程序员们会收到 Usenet 网际网络论坛上的麦金塔新闻群组内经过编辑的只读文章。这些文章有趣,可是不能响应其它程序员贴出来的问题相当令人泄气。程序员们总是问着在苹果公司出版的 Inside Macintosh 说明手册中已经清楚回答过了的问题,而网络上的程序员们总是用各种猜测性的答案代替文件中清楚的解答。幸运的,总有少部分熟悉 Inside Macintosh 的人会在没人正确解答时贴出正确的答案: " 看看 Inside Macintosh 弟四册,弟 32 页,那边说你该 ...." 。
如果你发现自己在测试一个问题的可能解法,那就停下来把你的说明手册打开来看吧。对,这并比不上到处修改程序有趣,也不比问别人怎么解决容易,可是你会对自己的操作系统更了解,更晓得该怎样在上头写程序。
神圣的时间表
有些程序员在被要求写出一个相当大的功能时,会花上两个月抱着键盘写程序,而从来没测试过自己写的东西跑得如何,其它程序员则会在停下来检查程序跑得好不好以前写出一堆小功能。只要这些程序员之后有彻底测试过他们的程序,这些做法都没做错。可是他们有彻底测试过自己写的东西吗?
想想一名程序员得在五天内实作出五种功能的例子。假设这名程序员有两种选择:同时实作跟测试各单一功能,或是写好五种功能再一次测试这五样功能。实际上,你觉得哪一种做法会产生出比较稳固的程序代码?多年来我看过两种程序写作风格,除了少数例外,那些编写程序边测试程序的人制造的问题比较少,我也可以告诉你原因何在。
假设一名程序员花了全部五天的时间来写出五种功能,却发现他没有足够的时间在时间表的限期内彻底测试过他的程序。你认为这名程序员会多花一两天来彻底测试过程序,或他会简单跑一下程序确定程序会动就了事了?答案如何取决于程序员与工作环境。不过后果就是让产品发行日延期,或是缩短测试时间。前者会让大部分公司都伤脑筋,而后者经常不会造成什么负面影响;而这名程序员大概会因为让产品如期上市而受到赞扬。不过只要有一个困难的功能,就可以把保留给所有功能的测试时间都用光了。
订下时间表的一个缺点是程序员们会将时间表当成比程序的测试工作更重要的事,就是说基本上,达成时间表上的目标比写出正确的程序更重要。我的经验告诉我,如果程序员能在期限内的时间里头把程序写出来,他就会准时把程序完成,而不管有没有把程序充分测试好。 " 此外 " ,他将认为, " 程序中如果还有任何未发现的问题,测试小组会让他知道 " 。
要消除这样的倾向,程序员们应该在继续进行下一个功能的实作以前,先把已经完成的部分逐一测试好。如果头三项功能要花五天来写,程序员们就得多花时间来写剩下的两样功能。他们也许会跳过剩下两个功能的测试以缩小时间表的延误,不过至少头三种功能已经测试过了,总比五种功能都没彻底测试过要好。
一小段一小段的写跟测试程序。
一定把程序测试过,即使那样作会让时间表出现延迟。
名称的意义
在 第五章 中,我解释过 getchar 的函式名称多常让程序员们认为这函式传回的是个字符值,实际上却是传回整数值。同样的,程序员们常相信测试小组该负起测试程序的责任,不然测试小组还要作什么?但是尽管许多程序员们那样认为,测试小组的工作并不是测试程序员们写出来的程序代码;测试小组的工作是保护公司,最后维护顾客使用的产品品质。
看看另一种产业:建筑业中的测试过程,应该能够更易于了解测试小组所扮演的角色。在盖房子时,承包商负责建设工程,验收员责检查工程有没做好,不过验收员并不负责测试整个工程。水电工人在铺好房子中的电线后,在离开前会先打开电源看看保险丝,并用电表检查每个插座能不能用。水电工人永远不会认为 " 我不用测试这些东西。如果有问题,验收员会让我知道哪边出了问题 " ,因为有这样念头的水电工人很快就会丢掉饭碗。
测试人员如验收员般不负责测试工作的好理由是他们很少碰得到需要的动到的东西,也很少有测试所需的工具或技能。尽管与普遍的观念互异,测试人员并不能比你对程序作出更好的测试。测试人员能在程序中加上除错检查来找出错误的资料流吗?他们能够如第三章中替内存子系统所作的,加上那些子系统测试吗?他们能用除错程序逐步检查你程序的执行路径与功能都如预期般动作吗?令人难过的事实是,能够确实比测试人员更有效的作到测试工作的程序员们往往没做该做的测试。
测试小组扮演了开发过程中重要的角色;不过并不是许多程序员们认为的那种角色。当测试人员检查一个产品时,他们会寻找功能上的缺陷与漏洞,核对产品是否与先前版本后向兼容,然后告诉开发团队哪些地方修改润饰过后可以改善产品,而且他们在 " 真实 " 环境中使用这些产品,确保产品功能真正有用,并回报任何他们注意到的错误情形。
即使你的测试人员除了找寻错误以外没作任何事,你还是不能假设他们会好好帮你把程序测试好。记住我在第一章中提到过的:测试人员只能够在程序的资料输入端猛灌一堆东西来看看问题会不会自己跑出来。当然,没人真的会想那样作,因为现在的测试工具似乎能够更科学的作到这点。不过实际上,现在的测试工具祇是更有效的执行那种测试方式而已。一名测试人员最多也不过能说 " 这程序似乎能动 " 。对我来说,那样子完全比不上程序员已经把程序整个追踪执行并观察过程序中的每条执行路线,来查对合法的输入产生出正确的输出。
此外,从实务角度来看,如果测试小组的某些成员经验不足,又会如何呢?或者如果你的测试小组因为更紧急的工作而抽不出时间来测试你的程序,那会怎样?这些情形在微软总是发生着;我相信别处也是如此。
重复的工作
如果程序员负起彻底测试程序的责任,有个问题自然会出现, " 这样子程序员跟测试人员不是在重复彼此的作过的事情吗? " 或许他们做的是同个性质的事情,可是当程序员们在测试程序时,他们是从里到外进行测试,而测试人员则是从程序外头往里头测试。
程序员们从每个函式开始测试,逐步追踪检查每条执行路线,查对程序与资料流的正确性。由这些步骤,他们逐步向外检查每个函式与子系统中其它函式是否正确运作。最后,程序员们使用单元检查来确定子系统间合作愉快。单元检查,作为额外的查核过程,规律性的检查测试过程中内部数据结构的状态。
测试员们将程序视为黑盒子,设计对程序的输入端扔出所有可能状态的资料来找寻程序缺陷的整体测试方式。测试员们也会使用回归测试来查对所有已知错误是否已经修好了而没再出现。从外部,测试员们逐渐往内部测试,使用程序代码涵盖工具来了解整体测试检查过多少内部程序代码了。测试人员用这些信息来建立还没测试过的程序代码所需要的测试方式。
这是用两种不同 " 算法 " 来测试程序的重要范例。这些做法用在一起会管用,是因为程序员们集中在程序的测试上,而测试人员在乎的是菜单现的正确性。藉由两个相反方向的合作,找出未知错误的机率就增加了。
你不能仅仅依赖测试小组来帮你测试程序-至少如果你想实在的写出零错误程序的话,你就不应该这样做。
不要依赖测试小组来找出你的错误。
戴白帽的测试员
你注意到过当测试小组找出一个问题时,一些程序员们是如何松了一口气的吗? " 哦! " 他们说, " 我当然高兴测试小组能在我们推出产品以前找出那个问题来。 " 其它程序员们则在测试员报告程序有问题时怨声载道,特别当测试人员回报了同一份程序的多个问题时。我看过程序员气得毛发直立的说: " 测试员为什么不饶了我? " 我还听过有项目主持人(他们应该更清楚怎样才是对的吧?)说, " 公开测试日期的延误都是测试小组的错。 " 有一次,我还为了这种问题,不得不将一名项目主持人跟测试小组主持人架开,免得他们打起来。
这听来很蠢吧?当我们没推出产品跟受攻讦的压力时,我们是可以很简单的坐下来,看出这样的行为有多荒谬。可是当你已经延误产品推出日期好几个月,而程序中仍然布满错误时,你就很容易将测试员们当成大坏蛋了。
当我看到程序员们对测试员们生气时,我会将他们拉开,然后问他们为何认为测试员们该为程序员们自己产生的问题负责。他们不应该对测试员们生气;测试员们祇是帮他们把自己产生的问题找出来而已。
当一名测试员回报你程序中的错误时,你的第一反应应该是难以置信的吓到了-你不应该预期测试员们会在你的程序中找出问题来。你的第二反应应该是感激他们,因为测试员们让你免于把一个问题呈现在使用者面前。
不要责怪发现你制造的错误的测试人员。
世界上没有笨问题
有时你会听到程序员抱怨说出现了一个奇怪的荒谬错误,或说测试员们经常回报笨问题。如果你听到程序员跟你抱怨笨问题的出现,停下来提醒他或她,错误的严重性跟值不值得修正并不是由测试员们决定的。测试人员必须回报所有问题,不管问题蠢不蠢,因为就他们所知,那些蠢问题一定是某些严重问题的副作用。
真正的问题不在一个问题笨不笨,而在为何测试程序的程序员没有抓出那个问题来。即使错误的情形很微小而不值得修正,找出问题的症结依然是重要的,这样才能让你避免相似的错误一再出现。
问题也许不大,但是只要有问题出现,就是严重的事情。
建立优先级
当你翻完本书,看到那些快速复习要点,你也许会惊讶其它有些是互相矛盾的。不过多思考一下,你也许就不会惊讶了。毕竟,程序员们时常在处理把程序写得又快又小的矛盾目标。
当你面对两个可能实作的选择时,你会选哪个?我想你在执行效率跟程序大小间作出选择并不会有问题-你总是在作着这类的决定-可是要怎么在执行效率跟维护性各见高下,或是小而错误百出跟大而异于测试的两个程序间作出选择?有些程序员们会不加思索的回答这些问题,不过其它人则会犹豫,而且如果你把同样问题隔了几周后再问,你还可能得到不同的答案。
那些程序员在面对不同性质代价时不能确定选择何者,是因为他们不了解在如程序大小与执行效率这样常见的条件何者为重所造成的。没有明确优先级目标设定的程序设计方式就像不知道终点何在的行进路线,在每个街角,你都会停下来问, " 我该何去何从? " 而你一定会走错路。
有的程序员相当清楚自己排定的各项条件的优先度,可是由于他们排定的顺序错误或互相冲突,使他们前进的方向还是朝着错误的路线。例如,许多老程序员对各种条件排定的优先级还是停留在 1970 年代晚期的想法,当时计算机内存相当捉襟见拙,而微电脑还跑得很慢。当时要写出一个可用的程序,你得以程序的维护性为代价来交换程序大小与执行速度。不过今天,内存空间大得很,而计算机已经快到即使执行很差的算法来作大部分的工作,都不会慢到令人不能忍受了。因此,这些条件的优先级倒过来了,再拿程序维护性来交换程序大小与执行速度已经事件不合理的事情了,因为这样子作的结果大概只会出现感觉不出执行速度加快了多少而却缺乏可维护性的程序。不过还是有些程序员将程序大小与执行速度的最佳化当成神圣不可侵犯的准则,使他们做出错误的实作选择,只因为他们对各项程序条件的顺序排定过时了。
不管你有没想过自己排定的条件顺序如何,或者你最近从来没想过这些条件之间的顺序排定,你得坐下来清楚的建立一份顺序列表(或如果你身为项目主持人时,帮你的团队安排一份列表),好让自己能够好好依据项目的这些目标作出最好的选择。注意,我说的是: " 项目的目标 " 。你的优先度列表应该反映的,不是你想做的,而是你应该做的事情。如果一名程序员将 " 个人表现 " 列为优先,那对这名程序员的产品会有什么好处?这名程序员会接受一个标识符命名标准,或接受别种程序区块排列的风格吗?
没有一个所谓 " 正确 " 的排列顺序方式,因为你选择的排列顺序就代表着你程序的风格与品质。看一下两名程序员 Jack 跟 Jill 的条件排列顺序:
Jack 的顺序排列 Jill 的顺序排列
正确性 正确性 整体效率 可测试性 程序大小 整体效率 区域效率 可维护性 / 程序清晰性 个人方便 一致性 可维护性 / 程序清晰性 程序大小 个人表现 区域效率 可测试性 个人表现 一致性 个人方便 这些条件的排列顺序对 Jack 跟 Jill 的程序有什么影响?两名程序员都将写出正确的程序当成第一优先,可是剩下的考量就不同了。如你所见, Jack 强调程序大小与速度,而不太在乎程序的清晰性,也没考虑过程序是否易于测试。
Jill 也将写出正确程序行为首要重点,不过不光是现在写出正确程序而已,她还考虑了未来程序的维护能否也正确进行。她只有在程序大小与执行速度对产品成败很重要时,才会担心这两种条件。 " 可测试性 " 在她的顺序排列清单中排得很前面,因为她相信除非程序易于测试,不然就不好查对程序的正确性(而程序的正确性当然是她的首要课题)。
对这两名程序员,谁比较可能
�         打开所有编译器警告选项来自动找出错误,即使这样会比较安静而不报告问题的解决方式耗去额外的程序大小与执行速度?
�         使用除错检查叙述与子系统除错检查?
�         追踪每条程序执行路径,仔细核对刚写好的新程序代码?
�         使用安全的函式接口而不用危险的接口方式,即使这样会在每次呼叫函式时多产生一两个额外的指令?
�         使用具可移植性的资料型态,并在位位移应该可用时改以除法或乘法处理(例如写成 /4 而不是 >>2 )?
�         避免使用 第七章 中提到过的那些抄快捷方式的技巧?
这是一连串不合理的疑问吗?我不认为如此。换个比较简单的问法, " 你认为这两名程序员中的哪一位,会在看完本书后,照著书中的建议去做? "" 哪一位程序员会看过 The Elements of Programming Style ,或任何一本建议人如何写程序比较好的书,并照著书中的建议去做? "
Jack 照着他自己的顺序排列方式,会将重点放在写出实际上会伤害产品正确性的程序上。她会浪费时间去尝试将每一行程序代码写得尽可能又小又快,而不会对产品的长期正确性放在脑海中想想。 Jill 刚好相反,照着她的条件设定,她会将重点放在产品的正确性上,而不是程序本身,除非有必要,不然她也不会在乎程序的大小与执行速度。
自我反省一下,程序的大小与执行速度表现跟产品的正确性,哪一个对你的公司比较重要。现在你觉得该将各种条件的排列顺序如何安排?
建立各种条件的优先级排列,并照著作。
呃,我不晓得
你曾经在看过别人写的程序后,想着他们为何那样子写程序吗?当你问他们为何那样写实,他们会说出像 " 呃,我不晓得我为什么那样写。我猜那时我觉得那样子写起来应该是正确的吧 " ?
我总是检查着程序,寻找各种帮助程序员提升程序写作技巧的方法,而且我发现这种 " 呃,我不晓得 " 的反应相当普遍。我也发现有这种反应的程序员们没有一个清楚的优先级设定;他们的决策方式,有时似乎祇是依据他们饭后的感觉好坏而已。有清楚的优先级设定的程序员们清楚知道他们该怎样选择实作方式,也能很快告诉你他们为何那样写。
如果你发现你常常不晓得自己为何把程序写成你写出来的样子,那就表示你真的得停下来建立一份属于自己的程序设计条件优先级排列清单了。
不要拿你不要的东西
我在本章中还没提到的一个重点是,你得发展出质疑自己写作程序方式的习惯。这整本书就是长久下来几个简单问题不断被思索后的结果:
�         我怎样自动找到这种问题?
�         我怎样预防这种问题的出现?
而在这结尾的一章中:
�         这种观念会帮助,或是妨碍我写出零错误程序的能力?
质疑自己的观念是重要的,因为这些观念就反映在你自己的程序设计条件优先设定上。如果你相信测试小组会好好测试你的程序,你想写出零错误程序就会碰到麻烦,因为你会认为,某种程度上,你可以省掉某些测试工作。想想:如果你不认为写出零错误程序是可能的,那你有多少机会写得出零错误程序?如果你不认为那是可能的,你还会去试着写出零错误程序吗?
如果你想写出没有任何错误的程序,你就得舍弃那些阻止你达成目标的观念。要舍弃那些观念的方法,就是问问你自己,那些观念对你写出零错误程序的能力有没有帮助,或是有无妨碍。
快速回顾
�         问题不会自己跑出来;问题也不会自己不见。如果你得到一份错误报告,可是你没办法重现那个问题,不要假设测试人员看错了。即使得把旧版本的程序代码翻出来,也不要放弃找出错误的努力尝试。
�         不要等 " 以后 " 再修正错误。重要产品因为错误百出而取消推出已经成了惊人的常见现象了。如果你在找到错误时立刻修正问题,你的项目就不会遭到同样的命运。如果程序总是接近零错误,你当然不会看到一份填满各种错误的臭虫清单。
�         当你追踪一个问题时,永远自我反省一下,这个问题是否只是一只更大只的臭虫的症状而已。没错,修正你找到的症状当然比较容易,不过你应该尽可能找出问题的真正症结所在。
�         不要写出不必要的程序或进行不必要的修正处理。让你的竞争者去写出那些花俏而不必要的功能,到处修改他们的程序,让他们去为了这些要付出额外代价的 " 附加 " 功能延误他们的产品推出日期,让你的竞争者们浪费时间在修正这些无用的程序代码中不必要的错误吧。 ?
�         记住,弹性并不等于易用。当你设计函式与功能时,注意它们的易用性;如果它们只是有弹性而已-如 realloc 函式跟 Microsoft Excel 中的颜色格式功能的话-这些功能并不会变得更有用;只是变得更难找出错误。
�         抗拒 " 试着 " 达到要求效果的诱惑,把你拿去尝试研究不确定事物的时间拿来找出正确的答案。如果必要,就打电话给你的操作系统厂商,找他们的开发支持小组问你想问的事情,这比起写出一个将来可能不能用的古怪实作要好多了。
�         把程序写成一小段一小段易于彻底测试的样子,不要省略测试过程。记住,如果你不好好测试程序,就没人有办法好好测试它了。不管你怎么做,不要期望测试小组能帮你把程序测试好。
�         决定出你的程序开发小组依循的程序设计条件优先级。如果你像 Jack 那样在乎程序最佳化,可是你的项目需要像 Jill 那样小心的人,你就得改变自己的习惯,至少在工作时得改变你自己。
�         学习计划:说服你的程序设计团队建立并依循一份他们同意遵循的条件优先级设定表。如果你的公司接受不同技能程度的雇员(像是入门新手,普通资历的程序员,资深程序员,或是功力高深得很可怕的人),你也许会考虑给不同人不同的优先级设定。你觉得为什么要这样做?