这是一本老书,作者 Steve Maguire 在微软工作期间写了这本书,英文版于 1993 年发布。2013 年推出了 20 周年纪念第二版。我们看到的标题是中译版名字,英文版的名字是《Writing Clean Code ─── Microsoft’s Techniques for Developing》,这本书主要讨论如何编写健壮、高质量的代码。作者在书中分享了许多实际编程的技巧和经验,旨在帮助开发人员避免常见的编程错误,提高代码的可靠性和可维护性。
不记录,等于没读。本文记录书中第八章内容:剩下的就是态度问题。
程序员有能力理解本书中的每一条指导原则,但如果没有正确的态度和一套良好的编程习惯,写出无错误 (BUG) 的代码将比预期困难得多。如果程序员认为错误可以简单地“消失”或认为“以后”修复错误对产品没有害处,错误就会持续存在。如果程序员经常“清理”代码,允许函数中不必要的灵活性,接受设计中冒出来的每一个“未规划”特性,或只是“尝试”一些随意的解决方案,希望找到能够工作的东西,那么写出无错误的代码将是一场艰难的战斗。拥有一套良好的习惯和态度可能是持续写出无错误代码的最重要的要求。
本书讨论的技术可以用来检测和防止错误,但这些技术并不保证一定写出无错代码。就像一支由技术高超的队员组成的球队也不能保证一定获胜一样。除了理解这些技术外,编写无错代码的另一个必要因素是一套良好的习惯和态度。
如果一个球队成天纸上谈兵而不实训、如果球队成员不断因为工资低而牢骚满腹,如果他们时刻担心被裁掉,那么这些一定会影响球员的成长和发挥。写出无错误代码也有类似的问题,要实践出真知,要有必胜的信心和良好的习惯。
本章指出一些编写无错误代码的主要障碍。
错误几乎不会“消失”
错误消失有三个原因:
- 错误报告不对
- 错误已被别人改正了
- 错误依然存在,但没有表现出来
作为专业开发人员,确定错误消失的具体原因是职责之一。
及时纠正,事半功倍
当我第一次参加 Excel 小组的时候,总是有进度和实现新功能的压力,但在修改错误方面,却一点压力也没有。直到某个未发布的产品因为失控的错误表而终止,这迫使 Microsoft 认真研究怎样开发产品:发现错误马上修改、发现架构问题马上重构。
完成项目的功能需求后再来修改错误或者重构,会有一些列问题:
- 修改一年前写的代码比修改几天前写的代码更难,也更费时。
- 错误发现的越早,就能越早从错误中学习,从而越早避免再发生类似错误。
- 放任错误来换取更快进度,会使得程序员轻视检查,因而产生更多错误,最后管理失控
- 错误太多时,预估交付时间变得困难
- 误导决策者。他们更关注功能,发现功能很快完成了,便可能急着推向市场
修改错误要治本,不要浮于表面
Anthony Robbins 在他的小说《唤醒巨人》中讲述了一个医生的故事。一天,一位医生在河边听到落水者的呼救声,她跳入水中,将落水者救上岸并进行抢救。这个落水者刚恢复呼吸,从河里又传来两个落水者的求救声。她再次跳入水中,将人救上岸并安顿好,然后又听到四个落水者的求救声,再然后是八个落水者的求救声……不幸的是,医生忙着救人,以至于没有时间去查明是谁把他们扔到水里。
你有过被BUG淹没的经历吗?你会思考为什么会有那么多BUG吗?
程序员经常忙于修复错误,但从没停下来思考是什么原因引起了这些错误。
在《糖果机接口》一章中,我们知道了 malloc
函数返回 NULL
违反了函数接口设计规则之一:不要在返回值中隐藏错误。程序员经常因为疏忽对 NULL
的处理而导致程序崩溃。
类似的,如果一个函数因为疏忽了对未预料的 NULL
处理而导致了崩溃,你会在这个函数中增加处理 NULL
的代码吗,就像下面的代码这样:
if(pb == NULL)
return FALSE;
这样做并不正确。这只是改正了错误的症状而没有改正错误的原因。因为错误的根本原因并不是函数没有处理 NULL
,而是那个会返回 NULL
的函数,因为那个函数设计的不合理。
我们再次强调函数接口设计的一个原则:设计可以引导程序员去做正确事情的函数接口。
一旦一个函数设计的不合理,就会迫使使用它的程序员承担额外的出错可能。那些有经验的或者从错误中学习过的程序员才能正确使用这个函数,总会有程序员在这个函数上出错,一个接一个。当出错时,我们是会联系函数的设计者,推动这个函数设计的更加合理,还是自责自己疏忽大意,然后在局部增加对未预料返回值的处理呢?
我希望你是前者。即便像 malloc
这样的既成事实的函数,我们仍可以使用一个包装函数,将其封装成更合理的接口,就像《强化你的子系统》一章中函数 fNewMemory
做的那样。
断病断因,治病治根。有时候BUG不断,可能是因为没有找到错误的根源。
无事生非
某些程序员总要强行在代码上留下自己的痕迹。比如喜欢将整个文件重新格式化以适合他们的口味。尽管大多数程序员对“清理”代码非常谨慎,但是,似乎所有程序员都不同程度地做过这件事情。
**清理代码的问题在于,程序员并不总是像对待新代码那样对待改进后的代码。如果你要修改现有代码,要确保:
-
进行测试。无论你认为修改有多简单。
用
'\0'
代替数字0
时,可能键入'0'
而改变程序逻辑;将局部变量hPrint1
改为hPrint
可能因为与全局变量冲突而造成程序失效。 -
对修改的代码逻辑了然于心。你看不懂的代码不要轻易动,因为这些代码可能有很好但又不明显的原因。
不要实现没有战略意义的新功能
如果没有必要,就不要编写或修改代码。要仔细考虑一个功能是否具有价值,优先做哪些对产品成败有重要作用的功能。有些功能对产品没有任何价值,它们只是:
- 为了填充功能集而存在
- 大客户的特定需求
- 竞争对手的产品有这些功能
- 某个决策者认为需要
- 某个程序员认为这很酷
- 某个程序员认为这很有技术挑战性
没有“零成本”的功能
所谓的“零成本”功能,是指在开发过程中无需额外努力便可以添加的功能。这些功能是另一个不必要的错误来源。零成本功能有一个大问题——它们几乎从未对产品的成功起到关键作用。程序员添加零成本功能是因为他们能够添加,而不是因为他们应该添加。毕竟,如果不需要花费任何代价,为什么不添加一个功能呢?但问题是,零成本功能对程序员来说可能花费不多,但成本不仅仅是编码:有人必须为这个功能编写文档、有人必须测试这个功能、还得有人修复这个功能中出现的任何错误。当我听到程序员说某个功能是零成本的,这告诉我他或她没有花太多时间考虑其中涉及的真正成本。
灵活性滋生错误
预防错误的一个重要策略是:排除设计中不必要的灵活性。
-
realloc
函数具备malloc
、free
函数的功能,具备扩展内存和缩小内存的功能,这是个过分灵活的函数。设计越灵活,就越难察觉错误。realloc
函数就难以验证参数的有效性,因为指针参数传入NULL
是合法的,块大小传入0也是合法的。 -
还有过分灵活的实现特性。最初的 HTML 文档推荐“宽容地接受数据”,也就是编写的网页即便不是严格遵守 HTML 的规范,浏览器也要尽量领会其中的意义。但是浏览器有很多种,每一种浏览器只接受规范中一个不同的超级,这就使得网页兼容所有主流浏览器变得非常困难。
“试一试”就是个屎
原文是
"TRY" Is A FOUR-LETTER WORD
,在英文语境里,“A FOUR-LETTER WORD” 是一句委婉的脏话,一般指 4 字母骂人的词,比如 shit 等。
当你寻求帮助,而别人建议你“试一试……”时,“试一试”中提到的的方案通常都不是可以采纳的合适方案。当别人告诉你试一试某件事情时,只是告诉你一个考虑过的猜测,并非问题的答案。
当程序员开始尝试某方案时,这意味着待解决的事情已经超出了他的理解范围,他会寻求任何有效方案。因为不理解尝试的方案,所以即使方案有效,也很可能会带来无意识的副作用,将来还要返工。
在找到正确的解法之前,不要一味地“试”,把时间花在寻找正确解决方案上。
如果你发现自己正在测试某个问题的可能解决方案,请停下来,拿出手册,然后仔细阅读。这可没有玩代码那么有趣,也没有向别人询问怎么试那么简单,但你将学到许多有关操作系统的知识,以及如何在它上面编程。
神圣的进度表
使用进度表的缺点是大多数程序员会优先考虑进度而不是测试。如果时间紧迫,程序员会牺牲测试时间来完成进度表上的任务。这意味着如果不给程序员足够的开发时间,程序员会牺牲质量。
一个程序员要用 5 天实现 5 个功能。这个程序员有两种选择:
- 实现一个特征就测试一个特征,一个一个地进行;
- 全部完成 5 个后,再测试
几年来,我考察了这两种编码风格。绝大多数情况下,边编写代码边测试的程序员较少出错。
尽量编写和测试小块代码。即使测试代码会影响进度,也要坚持测试代码。
我要再一次说明本书的时代局限性,本书出版于 1993 年,那时候还是瀑布流程为主的蛮荒年代。现在(2024年),我们应该都知道测试驱动开发,也很自然的编写和测试小块代码,而且可能是先编写测试,再编写代码。
不要依赖测试组去发现你的程序BUG
测试代码的责任不在测试员身上,而是程序员自己的责任。
测试人员不负责测试程序的主要理由是:他们不具备必要的工具和技巧。测试员不能加入断言来捕获有问题的数据流、不能在线调试程序、不能逐行、逐指令的观察代码和数据流程。
尽管公司可能设有独立的 QA 小组专门测试软件,但是开发小组仍然要把“QA 应该找不到任何错误”作为努力的目标。对于 QA 找到的每一个问题,开发团队都应该高度重视,认真对待。应该反思为什么会出现这种错误,并采取措施避免今后再犯。——《代码整洁之道-程序员的职业素养》
测试组并非无事可做,他们在开发过程中起着重要的作用,但绝不是程序员所想的那样:“还是先赶进度吧,反正测试组能测出所有 BUG,他们就是干这个的”。
程序员测试代码,是从里向外。他们总是从测试每个函数开始,逐行逐指令的通过各条代码路径,验证代码和数据流,然后逐步扩大测试范围:验证函数能够在子系统正常运行、验证子系统之间能够正确配合。
测试员测试代码,是从外向里。测试员把代码作为一个黑盒,从程序各个输入处进行测试,观察输出,寻求其中的错误。测试员也可能利用回归测试来证实所有报告的错误都已排除。然后,测试员逐步向里推进,利用代码覆盖工具,来检查未执行到的代码。
这是两个不同的测试,程序员测试强调的是代码,测试员测试强调的是功能。两者从不同的方向考虑问题,能增加发现未知错误的机会。
每当测试员在你代码中找出一个 BUG 时,你的第一反应应该是震惊和怀疑,因为你自己会严格测试代码,你不认为测试员还能发现 BUG;你的第二反应应该是表示感谢,因为测试员帮助你避免了交付错误。
测试员无法判断 BUG 的严重性或是否值得修复。测试员必须报告所有 BUG,无论是否愚蠢,因为据他们所知,这些愚蠢的错误可能是严重问题的副作用。
真正的问题不是 BUG 有多愚蠢,而是为什么程序员在测试代码时没有捕获到这个 BUG。你或许会说这个 BUG 不重要也不值得修改,但确定其原因仍很重要:防止类似的 BUG 再次出现。
BUG或许很微小,但它能出现是严重的问题
小结
- BUG既不会自己产生,也不会自己修复。如果你收到一个BUG报告,但是你不能重现BUG,不要假设测试员产生了幻觉。努力去查找错误,甚至恢复到旧版本测试。
- 不要推迟修复BUG。一个主要产品,因为失控的BUG列表而被取消掉,这种情况正变得非常惊人的普遍。如果你发现BUG就马上修改它们,你的项目就不会遭受毁灭性的命运。如果你的项目一直保持近乎0个BUG,那怎么可能有失控的BUG列表呢。
- 当你发现一个的BUG,务必问问自己:这个BUG是某个严重BUG的征兆吗?修复跟踪到的表面BUG是容易的,但是你总是应该为找到真正原因而努力。
- 不要编写不必要的的代码或进行不必要的修改。让你的竞争对手去实现看上去很酷但毫无价值的产品功能、去做不必要的代码清理,因为实现未规划的产品功能(“free” features)而推迟交付日期。无用的代码产生没有必要的BUG,让你的竞争对手去浪费时间修改这些BUG。
- 记住灵活与易用并不是一回事。当你设计函数和产品功能时,将关注点放到容易使用上。如果仅仅只有灵活,就像
realloc
函数那样,那么灵活性并未带来更多益处,相反它们会让错误更难发现。 - 不要病急乱投医。胡乱的尝试某个方案然后期望能达到理想效果,要抵制这种想法。把时间花在寻找正确解决方案上,而不是尝试上。如果必要,联系操作系统厂商,找他们的开发支持小组。这比提出一个奇怪的实现,然后将来再返工好的多。
- 函数应该足够小以便彻底地测试,不要克扣测试时间。记住,如果你不测试你的代码,可能就再也没人测试了。无论如何,不要期望测试组专为你测试代码。
- 确定组内开发项目所遵循的优先顺序,并严格执行。比如某项目组正确性列为最高优先级,其次是可测试性、全局效率、可维护性、一致性、大小、局部效率、个人编码风格。
每一份打赏,都是对创作者劳动的肯定与回报。!