这是一本老书,作者 Steve Maguire 在微软工作期间写了这本书,英文版于 1993 年发布。2013 年推出了 20 周年纪念第二版。我们看到的标题是中译版名字,英文版的名字是《Writing Clean Code ─── Microsoft’s Techniques for Developing》,这本书主要讨论如何编写健壮、高质量的代码。作者在书中分享了许多实际编程的技巧和经验,旨在帮助开发人员避免常见的编程错误,提高代码的可靠性和可维护性。
不记录,等于没读。本文记录书中第四章内容:对程序进行逐步跟踪。
前两章我们讲到了
断言
和为子系统设防
这两种自动化检查方法,这两种检查措施都很有价值。我们也说过,不要等待错误发生。要“挨门挨户”的搜查错误:在程序中加上能够积极地寻找这种问题的调试代码。
这里隐含着一个条件,你必须知道哪里可能会发生错误,也就是说:事先,你得知道错误“长什么样”。如果你不知道错误是什么,就抓不到这个错误。比如你只在门窗上安装了报警器,那么当小偷从地下室的开口进入,就不会引起警报。
保证家中物品不被偷走的可靠办法是:在小偷有可能光顾的期间内呆在家里。防止错误进入程序的办法也是这样,在最有可能出现错误
的时候,密切关注。因此,找到错误的最佳方法是使用调试器逐步执行所有新代码。通过逐步执行每条指令,关注数据流,可以快速发现表达式和算法中的问题。专注于数据而不是指令,可以从另一个角度来审视代码。逐步调试代码虽然花时间,但远没有大多数程序员想象的那么耗时。
那么,什么时候是 最有可能出现错误
的时候?
答案是:在编写或修改程序的时候,最可能出现错误,我们要认识到这一点的重要性。编写无错代码的最好办法是在编译时对其进行详尽的测试。
我们在第一章中介绍了 黑箱
测试。测试者给程序提供大量的输入,然后检查输出,如果测试者认为相应的输出没有问题,那么则认为程序没有问题。
黑箱测试的缺陷在于,除了提供输入和检查输出之外,测试者再没有别的方法对代码进行测试。给一些输入,得到了正确的输出,并不能保证代码是正确的。就像你问一些问题,倾听对方回答,并不能确定对方是不是疯子,因为你不知道对方脑袋里在想些什么。
上面的这些限制对程序员来说都不是什么难事。程序员可以在代码中设置断点,一步一步跟踪代码的运行,观察输入变为输出的过程。程序员测试程序最好的办法是对代码进行逐条跟踪,对中间的结果进行认真的查看。
不要等到出了错误再对程序进行逐条地跟踪!
-
对正常处理程序逐条跟踪
-
对错误处理部分逐条跟踪
-
对程序中每一条可能的路径逐条跟踪(明显的路径:
if
、switch
,其它路径:&&
、||
、?:
)
习惯于对代码进行逐条跟踪会产生一个有趣的现象,即他们很快就会学会编写较小的容易测试的函数,因为对大函数进行逐条地跟踪非常痛苦。
对代码进行逐条跟踪的真正作用是:可以使我们观察到数据在函数汇中的流动
如果单步执行仍不能得到执行的细节,那么对于关键部分的代码要进行汇编指令级的逐条跟踪。
小结
- 代码不会自己生出错误,错误是程序员的产物,编写新代码或者修改现有代码都可能产生错误。如果想发现代码中的错误,最好的方法是逐条跟踪代码。
- 逐条跟踪代码并不会花费太多时间,与返工带来的影响相比,这点时间划算的很。
- 一定要对每一条代码路径进行逐条的跟踪,尤其是错误处理部分。
&&
、||
和?:
运算符,它们每个都有两条代码路径需要测试。 - 当需要汇编级别的逐条跟踪时,不要回避。
题外话
本书第一稿出版于 1993 年,当时软件工程领域还没有 敏捷开发
、测试驱动开发
等概念(这些概念大范围被认可是在 2000 年左右)。作者写这本书的时候遵循一个理念,那就是只介绍那些并不广为人知,没有被广泛实践,也没有被广泛使用的概念和开发哲学。那个时候作者已经认识到测试的重要性,提倡编写较小的容易测试的函数。在当今 (2024 年) 来看,这是再平常不过的事情,甚至,对于“防止错误进入程序”,我们也有了更号的方法,那就是 测试驱动编程
。我在之前的博文《C 嵌入式系统设计模式 01:软件开发概述》中描述过自己对测试驱动开发的见解,现摘录如下:
开发无缺陷软件的最新技术是一种称为
测试驱动开发
(TDD) 的敏捷实践。
无缺陷的软件
(defect-free software)表示该软件没有任何错误、缺陷或问题,能够按照预期的功能和需求正常运行。
“开发无缺陷的软件?这怎么可能!”换做以前的我,不假思索地就会如此断言。但那时的我,从没有想过,我给出的这个结论,依据是什么。事实上,我没有依据,我连脑子都没动过一下,就草率地下了这样的判断。
而现在,我开始反思这个问题,一方面是因为我的思维方式发生了转变,不再轻易地给出片面的结论;另一方面,是因为我尝试了测试驱动开发。尽管我依然认为开发完全无缺陷的软件是一个难以企及的目标,但测试驱动开发确实让我看到了这一目标的可能性。它让我对我的软件质量越来越有信心。
在尝试测试驱动开发的早期,最难以理解、最难以坚持的可能是被称为 Bob Martin 的测试驱动 3 条原则:
- 不允许编写任何产品代码,除非它可以让失败的单元测试通过。
- 不允许编写任何足以导致失败的更多的单元测试,编译失败也算失败。
- 不允许编写任何足以让单元测试通过的更多的产品代码
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
首先,你需要先编写单元测试,这就是为什么叫做测试驱动的原因之一。但是根据规则 2,你不能写太多的单元测试,一旦单元测试代码无法编译或者断言失败,就必须编写产品代码。但是根据规则 3,你只能编写恰好使单元测试编译通过或者断言通过的产品代码,而不能再多!
当我年轻时,我曾尝试测试驱动编程,先后尝试过两次,都因为第 2 和第 3 条原则而放弃,我觉得它很愚蠢!如果你没有尝试过测试驱动开发,也许无法体会到我当时的感受。没有关系,我只是想分享一下我曾经的态度和后来的变化。
随着年龄的增长,我的思维方式发生了转变,我收起了排斥和轻视的态度,放下成见,严格遵守测试驱动原则来编写一个程序。然后,我被测试驱动开发迷住了。
我开始认识到,那些我曾经认为愚蠢的规则,实际上并不愚蠢,而是构建稳健软件必不可少的基础,坚持这样的原则,能确保我的代码在全面且细致的测试用例中得到验证,换句话说,能产生彻底测试过的产品代码。
彻底测试,是每一个软件开发者的理想目标,只有达到这一条件,我们才有可能触及编写无缺陷软件的可能。很多时候,我们都在竭尽全力追求彻底测试,但受限于大脑的逻辑和记忆能力,总是会遗漏某些测试路径。然而,遵守测试驱动开发的 3 条原则,可以帮助我们系统化地进行测试,确保每一个细节都得到了验证。只是遵守 3 条原则,就能实现如此的效果,这真是太划算了。
题外话 2
如果你对测试驱动感兴趣,想详细了解,推荐阅读《测试驱动的嵌入式 C 语言开发》。这本书的原作者和译者,给予他们再多的赞美都不为过。
每一份打赏,都是对创作者劳动的肯定与回报。!