软件构建(九)——协同构建与调试

所有的协同构建技术都试图通过这样或那样的途径,将展示你工作的过程正式化,以便把错误暴露出来。软件构建不可避免地都会伴随着调试,在一些项目中,调试可能占到整个开发周期的50%。对很多程序员来说,调试是程序设计中最为困难的部分。本文的前半部分将介绍协同构建的一些实践方法,后半部分将介绍一些科学的调试手段以节省更多精力。

1. 协同构建

1.1 协同开发实践概要

“协同构建”包括结对编程、正式检查、非正式技术复查、文档阅读,以及其他让开发人员共同承担创建代码及其他工作产品责任的技术。各种协同构建技术之间尽管存在着一些差异,但它们都基于一个相同的思想,那就是在工作中开发人员总会对某些错误点视而不见,而其他人不会有相同的盲点,所以开发人员让其他人来检查自己的工作是很有好处的。

大量研究和数据表明,协同开发在捕获错误方面比测试的效能更高,而且让人们意识到他们的工作会被复查,这样他们会小心谨慎地检查自己的工作。此外,协同构建有利于传授公司文化以及编程专业知识,复查是老人培养新人人以提高其代码质量的好方法。

1.2 结对编程

在进行结对编程的时候,一位程序员敲代码,另外一位注意有没有出现错误,并考虑某些策略性的问题,例如代码的编写是否正确,正在编写的代码是否所需等。全程采用结对编程的成本可能比单人开发要高大约10%~25%,但开发周期大概会缩短45%。结对编程与单独开发相比,能够使人们在压力之下保持更好的状态,能够改善代码质量,缩短进度时间表等等。要从结对编程中获益,需要遵守以下几条准则:

  • 用编码规范来支持结对编程:应提前制定标准,避免两个人把时间浪费在争论代码风格上
  • 不要让结对编程变成旁观:不掌握键盘的那个人应该主动参与到编程当中,例如分析代码,提前思考接下来的代码应该做些什么,对设计进行评估,并对如何测试代码做出计划
  • 不要强迫在简单的问题上使用结对编程
  • 有规律地对结对人员和分配的工作任务进行轮换
  • 鼓励双方跟上对方的步伐
  • 确认两个人都能够看清楚显示器中的代码
  • 不要强迫程序员与自己关系紧张的人结对
  • 避免新手组合
  • 指定一个协调工作分配的组长,其对结果和项目外其他人的联系负责

1.3 正式检查

正式检查(详查)是一种特殊的复查,与普通复查的区别是:详查关注的是复查者过去所遇到的问题,专注于缺陷的检测而非修正;复查人员要为详查会议做好预先准备,并且带来一份他们所发现的己知问题的列表;详查的主持人不是被检查产品的作者,且应该已经接受过主持详查会议方面的培训;只有在参与者都做好充分准备之后才会召开详查会议,每个参与者都赋予了明确的角色(主持人、代码作者、详查评论员、记录员、只了解结果的经理);每次详查所收集的数据都会被应用到以后的详查当中,以便对详查进行改进;高层管理人员不参加详查会议,除非你们正在详查一个项目的计划,或者其他管理方面的资料,但技术负责人可能参加。

详查由以下几个阶段组成:

  • 计划:作者提交设计或代码,主持人决定参与的人员和时间地点
  • 概述:当评论员不熟悉他们所要详查的项目时,作者可以花大约一个小时来描述一下这些设计或代码的技术背景
  • 准备:每一个评论员独立地对设计或者代码进行详查,找出其中的错误。给评论员赋予特定视角或待详查场景,可以有效提高复查的效率
  • 详查会议:评论员阐述设计或阅读代码,记录员记录发现的错误。不要在开会的过程当中讨论解决方案,小组应该把注意力保持在识别缺陷上。详查速度,可以参考系统级代码每小时90行,应用级代码每小时500行
  • 详查报告:主持人总结报告,列出每个缺陷及其类型和严重级别
  • 返工:主持人将缺陷分配给作者修复
  • 跟进:主持人负责监督在详查过程中分配的返工任务

对于作者来说,详查的过程应该是正面的,所有参与者都是一个学习的过程。作者也应该预料到他会听到对某些缺陷的批评,不应该试图为正在被检查的工作辩护,在复查之后,作者可以独自对每一个问题进行思考,判断它是否真的是一个缺陷。

1.4 走查

走查是一种很流行的非正式复查方式,同时也是一种宽松的定义。既然是一种较为“随意”的复查形式,其找出程序的错误概率也相对较低,而当项目的压力增加的时候,走查更是变得几乎不可能。与走查相比,详查在消除错误方面似乎更有效。但如果你有个很大的复查团队,或者由其他组织的评审员参与,或许走查会更适合。

2. 调试

调试本身并不是改进代码质量的方法,而是诊断代码缺陷的一种方法。软件的质量必须从开始逐步建立:开发高质量软件产品的最佳途径是精确描述需求,完善设计,并使用高质量的代码编写规范。调试只是迫不得已时采用的手段。

程序不可能没有缺陷,关键是掌握避免缺陷产生的方法,并从以往的缺陷中学习。程序员在调试过程中应该理解正在编写的程序,明确犯了哪种类型的错误,从代码阅读者的角度分析代码质量,审视自己解决问题和修正缺陷的方法。

2.1 效率低下的调试万法

下面列举的是调试的魔鬼指南,注意这些是让程序员们受尽折磨的传统的调试方法,应该引以为戒

  • 凭猜测找出缺陷:把print语句随机地散布在程序中,凭输出来确定缺陷到底在哪里。如果通过print语句还是不能找到缺陷,那就在程序中修改点什么,知道有些东西好像能干活了
  • 不要把时间浪费在理解问题上:要解决它们并不需要彻底弄懂程序,只要找出问题就行了
  • 用最唾手可得的方式修正错误
  • 迷信式调试:也许你会遇到这样一种程序员,他们经常碰到各种奇怪问题——不听话的机器,奇怪的编译器错误,月圆时才会出现的编程语言的隐藏缺陷,失效的数据……要知道,如果你写的程序出了问题,那就是你的原因,不是计算机的

2.2 科学的调试方法

下面给出一种寻找缺陷的有效方法:

  1. 将错误状态稳定下来
  2. 确定错误的来源
    a. 收集产生缺陷的相关数据
    b. 分析所收集的数据,并构造对缺陷的假设
    c. 确定怎样去证实或证伪这个假设,可以对程序进行测试或是通过检查代码
    d. 按照2(c)确定的方法对假设做出最终结论
  3. 修补缺陷
  4. 对所修补的地方进行测试
  5. 查找是否还有类似的错误

如果一个错误无法重现,这通常会是一个变量初始化错误,或者是一个同时间有关的问题,或者是悬空指针问题。要将一个错误的发生稳定下来,要构造一个尽可能简单的测试用例,并假定一些产生错误的因素,用测试用例一个一个排除掉无关的因素,不断缩小错误因素的范围。一些寻找错误的小建议:在构造假设时考虑所有的可用数据,提炼产生错误的测试用例,采用多种不同的方法重现错误,用更多的数据生成更多的假设,将代码分而治之缩小嫌疑代码的范围,对之前出现过缺陷的代码和最近修改过的代码保持警惕。

在必要时,也可以采用蛮力调试。人们往往出于投机心理都宁愿去用一种有可能在五分钟内发现缺陷的高风险方法,也不愿意为某种保证能找出缺陷的方法花上半个小时。实际上当你被这种心理绕进去时,有可能几个小时甚至几天就这样浪费了。如果打算通过捷径摘取胜利果实,那么应该为尝试捷径的时间设置一个上限。如果耗时超过了上限,就应老老实实地承认问题比你最初想象的要更加难于分析,应该转到蛮力(彻查代码甚至重写)的路上重新开始。

对于语法错误,虽然编译器现在做得越来越好,这个问题不应成为一个很大的障碍,但也应该注意一些编译器的坑,例如:不要过分信任编译器信息中的行号,有时问题可能出在那一行的前后;不要迷信编译器给出的错误信息;不要轻信编译器的第二条信息,如果无法迅速找出第二条或第三条错误信息的源头,先把第一条处理了再重新编译看看。

2.3 修正缺陷

  • 在动手之前先要真正理解问题
  • 理解程序本身,而不仅仅是问题
  • 花点时间编写测试用例,验证对错误的假设
  • 修改错误后不要急着提交,可以先放松一下,等到充分考虑了这样的修改完全无误后再提交
  • 保存最初的源代码,以方便对照
  • 治本,而不是治标,不要用特例去绕过程序的错误
  • 修改代码时一定要有恰当的理由
  • 一次只做一个改动
  • 检查自己的改动并增加能暴露问题的单元测试
  • 修正缺陷后,搜索类似的缺陷

2.4 调试工具简介

  • 源代码比较工具:如diffBeyond Compare等,常用于比较新旧代码的差异以唤醒记忆
  • 编译器
    • 将编译器的警告级别设置为最高级,尽可能不放过任何一个警告
    • 用对待错误的态度来处理警告:一些编译器允许将警告当做错误报告
    • 在项目组范围内使用统一的编译设置
  • 增强的语法检查和逻辑检查:如各种语言的lint工具(一般许多高级的编辑器都有对应插件)
  • 性能分析器:有时花上几分钟来研究某个程序的性能分析结果,或许可以让你发现一些令人惊奇的隐藏错误
  • 测试框架/脚手架:各种语言都有对应的测试框架
  • 调试器:利用好编译器可能具有的以下功能:
    • 设置断点,某行代码执行n次或变量被赋予特定值时中断,监控变量,逐行运行代码,记录特定语句的执行
    • 检查结构化和动态分配的数据,智能地适应用户定义的数据类型,在运行过程中修改值并继续运行
    • 查看高级语言生成的汇编代码,查看调用链
    • 针对每个单独的程序保存调试参数(如断点、监视变量等)

参考文献:电子工业出版社《代码大全(第2版)》第21、23章

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器