前段时间陆陆续续读完了pangba老大力荐的Why Programs Fail, 一本关于程序调试的大作。给人最大的启发就是调试是一门科学而不是艺术。我从来不知道,原来调试还有那么多的千奇百怪的工具,可以写那么多的数学推导。从 小学开始我们就开始了解,作文没写好你可以说老师欣赏不了,而数学题算错了只能怪自己混的不够到位。于是我们总是很主观的把调试当成艺术来看待,调试能力 的高低不仅取决大量经验的积累也是天分所致。很遗憾的是,本书中告诉你,调试是有方法可以寻求的,像解数学题一样,用既定的方法套路去做,无论天赋高低经 验多寡,都能得到正确结果。恩,好像说的有点夸张了。但至少,调试是有章而寻的,是无须质疑的。我想我会努力去尝试使用一些书中提到的工具,但我依然不甘 心把一个充满所谓艺术性的活干的如此机械,书中的一些经验总结一下,同样可以在无足够工具支持下快速的进行调试。
TRAFFIC。这是作者把调试各步骤首字母提取出来的得到的一个词。依次对应跟踪问题,重现问题,自动化和简化,寻找感染源,专注可能的来源,分离感染链
问题跟踪,一个极其重要的内容是,错误是谁发现的。个人觉得,谁发现的直接影响到错误的可重现度。按可重现度高低来排,依次应该是自己发现》测试人员发 现》用户发现,而错误的可重现度,是影响调试时长(甚至是成功与否)的重要因素。换句话,如果想很好的调试,应该尽早的让潜在的错误跳出来和你对话,所有 为尽早发现错误进行的努力都是值得的。这些努力包括:断言,异常(你为什么不使用异常?),单元测试,重构,代码走查,静态分析等等。当你写下一些code的时候,你应该多考虑一点,如果出错,我是否能很快知道是这里出来的,这就是一个很好的习惯。
日志,我觉得其最重要的作用是用于重现问题阶段。它不应该作为个人编码阶段定位错误点、分析bug的最优先选择(当然这有太多的意外情况,比如你有很好的 日志分析和记录系统,或者没有其他更好手段可以使用等等...),而是应该作为重现客户问题的重要手段。在问题重现中,我们总是希望错误能在我这里明明白 白再出现一次,但很多时候这样小小的渴望都沦落成为了奢求。我曾做一个外包项目,在这里跑的好好的程序到美国那边总是出错。介于保密的原因,很多东西我们 不能够过多的了解,甚至连真实的数据我们都拿不到。这时候我们能够依仗的就是各种日志文件。通过日志,你可以在不重现错误的前提下推断出错误,不知其然却 知其所以然,未必不是解决问题的方法。
更多的时候,我们能够重现错误,但重现步骤过于繁杂(比如客户告诉你,他是一边倒立一边点鼠标发现这个错误的...),不能够帮我们很快定位在真实的错误 起始点上。这时候就需要进行简化,用到的是作者看家的兵器delta技术,有兴趣的可以翻书查看,而论其思想,主要是二分思路,或者说有条理有步骤的剔除 非相关因素。有条理有步骤是最关键的词汇,二分只是其中一种很步骤很条理的方式,而其他的,我觉得只要是够有规律,能够达到循序渐进,不走弯路的,都行。
书中描述了所谓的科学调试的方法。基本是按照回溯的路线(及从错误的出现点出发寻找错误的发起点...),提出假设,观察,验证或推翻假设,一直将假设等同于错误的起点为止。我觉得这也只是有步骤有条理的方式的很好的一种,而不是全部。
只是规规矩矩走路的人往往会错失捷径。我们很多时候看到这个错误出现,根本不用一步一步回溯,根据我们的经验和奔涌的肾上腺素,就能快速而准确的猜出问题 的原因,省时省力(该方法被称为快速而杂乱的调试...)。我想这是很多人(也包括我吧...)发掘问题起因的最习惯手段。而世界的残酷在于,一旦我们沉 迷于我们自己的猜测,会深陷其中,无法自拔,赌徒似的把大把大把的时间投进去,却血本无归。一个标准是,如果进行了十分钟的杂乱的调试依然没有找到问题的 话,就转入科学调试状态(把自己做成一个快表系统...),鱼和熊掌,就是这样来兼得。
此外,我们还有很多调试的手段,我觉得从目的上看可以分成事前推测和事后分析(还是那个WS的比喻,套&&药...~_~)。一个错误出 来,我们可以先走查一下代码,看看编译器的warning等等。书中最NB的,是一种静态分析技术,通过工具解析代码结构,得到一些可能的错误起因。在我 眼里,这就是所有事前分析手段的专业版本,有空我一定要玩玩这些工具。可惜的是,我们总是不能聪明到看(或自动分析)代码就可以了解所有错误的程度。很多 东西只有run起来,才有恍然大悟的机会。所以我们需要很多事后分析手段。用的最多的还是调试器。日志,对,也可以用在这里。两者最大区别时,调试器是精 确慢速而局部的,日志是粗犷全面但部署困难的。两者的使用,和场合和工具的犀利程度有很大关系。对我个人来说,在二者皆宜的场合,我会动用调试器。
说到调试器,不得不说测试(这里指的是程序员个人的单元测试...)。在此前一点概念需要说明。在很多人概念里,调试简单的对应使用调试器,与测试是格格 不入的。而在本书中,调试,指为了发现错误的起因所使用的所有手段,其中包含测试。测试最最最最优良的一点在于它是自动化的。或者说是可永远重现的。这就 是说,使用测试,可以帮你解决诱发错误,重现错误等及其麻烦的事情。但是,测试往往只是能够帮你找到错误的出现点,而不总是能快速的把你带到错误的起因 点,而为了部署这些测试你需要花费很多的精力。正是基于此,包括云风老大等很多人不屑使用测试(再次强调,这是指个人的单元测试...)。但是,这部以为这测试不重要,而成为你可以肆意偷懒放低代码质量的借口。而是因为其带来的好处被其他的一些技术抵消了,这样的话,为其付出的代价就会显得很沉重。
但是无论你使用什么样的手段进行调试。代码结构的质量才是最最最根本的内容。一个函数划分很细致,不大量滥用全局变量的代码,使用很多无副作用的函数,通 过测试你发现错误出现点,往往你就能很快到达错误起点(因为你把错误可能出现的地方限定在了有限的范围)。而如果你代码结构混沌不堪,函数/类之间关系错 综复杂,哪怕你用测试找到了错误出现点,开着调试器进去看,你也会很快转晕。所以提高代码质量,合理应用适合的调试工具和手段,正确使用调试的方法才是正 道。在这里,不得不提一下TDD。最初的时候我总觉得TDD最核心的是T,Test。后来才开始明白,它最核心的其实是D,Drive。你可以把测试写的 很弱,但你一定要在此影响下把代码重构的很好。由此得到一个蛮歪的理:如果你或你团队的代码素质很高,可以尝试不用TDD的一些开发手段;但如果你或你团 队代码素质不够高,请把自己套在TDD里面磨练一下。之所以说歪,是因为我其实只想说后半句,前半句纯属为了对仗工整而用。
最后我发现我忘提一个很好玩的东西,断言。我一直觉得这个东西很有意思。是一种游离在单元测试与调试器之外的手段,是一个隐藏在事后分析中的事前推测派来 的间谍。assert的意义在于猜测并提醒一些最有可能的错误,它不精确但也不死板。我一直觉得,全面合理的铺下断言这张网(三个部分,输入输出不变 式),可以很有效的提高调试速度。