用医生的思考方式调试你的代码
“现在的编程工作就像是对你需要解决处理的部分做科学研究。”
——Gerald Sussman
设计和维护好的软件就像是一个抵制复杂度的永无止境的奋斗过程。任何足够大小的应用程序的代码路径和组件都能迅速成长成令人眼花缭乱的组合爆炸。
一点都不简单。
当部署类似于Heroku和AWS的平台时,单服务器的Web应用程序成为了分布式系统。现代浏览器模糊了客户端和服务器之间的界线。当简单程序在多个CPU内核上运行时,它们就会成为复杂的协调问题。虽然像测试驱动开发等实践和SOLID原则等指导,可以帮助我们模拟问题,简化解决方案,但大多数软件应用程序都是一些复杂的系统,每个组件也会以意想不到的方式进行交互和组合。
当软件系统中发生意外情况时,会造成很严重的后果。幸运的是,软件开发人员可以借鉴另一门更古老的学科,来应对对于复杂系统的关注、维护和调试,这门学科就是:医学。
鉴别诊断是医生用来匹配系列症状及其可能病因的系统化方法。一个好的鉴别诊断包括以下4个步骤:
- 列出所有观察到的症状。
- 列出可能的病因。
- 按轻重缓急给这些病因排名。
- 按照优先顺序进行测试,以排除病因。
虽然上面这4个步骤是为医生而整理的,但是我们同样可以像一个医生一样思考,用一种强有力的方式来找到并消除软件缺陷。将诊断过程分解为一个一个目的单一的步骤,确保每个步骤都能得到应有的重视。按照优先顺序是为了保证专注检查的重点,并作出务实的干预措施。然后进行测试,排除假设,以确保调试的严谨。
白板是个好东西
当错误发生时,我们大多会想也不想地立马去调查最可能的原因。懂得向后跟踪和少许背景知识,人性就会趋向于投机主义。但是好的诊断始于列出的症状,而不是病因。写下可以观察出来的所有症状,无论是异常处理,还是错误代码,哪怕只是异常的行为,都可以。可以使用文本编辑器或者白板,但是,你最好能对诊断过程中的每一个步骤做笔记,这很重要。从假设出发分开观察,有助于确保你不会排除或忽视潜在原因。并且多数时候,列出更多的症状反而会缩小可能范围,避免你将时间浪费在测试不正确的假设上。
写好了一系列症状,那么接下来就可以开始考虑原因了。
斑马和马
“当你听到马蹄声的时候,找的应该是马,而不是斑马。”
在应用程序中出现代码bug的可能性比在Web框架中出现bug的可能性要大,而在Web框架中发现bug又比在操作系统中发现bug更容易。当然让别人来审查代码是个好主意,但事实是,大多数bug审查起来特别无聊。所以在开始考虑进阶到更复杂的问题之前,先给出最简单的解释。
话又说回来,正如同一个症状却又可能是完全不同的病因引发的,所以我们应该将所有能想到的相关病因都写下来。就像原先我们对症状直接描述为“what”,后来用“how”区分开来,头脑风暴解释法的目的是用“how likely”来区分“how”。捕捉任何看似合理的要点,以便于节约分析。
重中之重,不能有害
鉴别诊断与其他的演绎方法不同,因为医生必须不断地评估风险,并权衡对病人生命的影响。当然如果我们的产品中存在着bug,虽然不会像医生那样负有生命责任那般严重,但是停顿修复会产生既现实又痛苦的成本费用。就像威胁生命的疾病事件一样需要立即进行干预,严重的bug可能需要粗暴的简单修复,例如回滚和重新启动。将假设按优先顺序排列,然后再考虑权衡,并判断决定是否启动测试假设或立即进行干预。
准备图表
正如患者会有医院病历和其他背景信息的图表,你的软件系统可能也需要具备图表。从日志和错误报告系统收集信息,来说明你的分析。至于系统指标和跟踪误差,你不妨将它们当作是明智的预防性药品。
如果你的病人尚未处于严重危险之中,那么可以先进行假设-演绎。从你定义的优先级最高的假设开始,一个一个地证明它们是错误的。虽然支持性证据有时候或许能有助于你找到bug的所在,但是失败的测试驱动了演绎过程。这乍一看上去似乎有悖直觉,但是测试-消除假设策略是追溯bug到它的起因的最快方式。在许多情况下,一些简单的测试就可以一次消除几个假设。当然,也有时候,为了否决假设你就得执行更多的测试。
实验室工作
不同于医疗世界的令人难以接受,只要你愿意,你随时都可以克隆软件应用程序,执行可怕的人体实验。如果你有足够的信息来触发你要诊断的bug,那么可以将它复制到受控环境中,例如一个有着最新数据库备份的临时服务器。当你消灭原因,收集到新的数据,并完善假设之后,你的bug的真正原因线索将变得更加清晰。
清楚地思考复杂系统需要的关心和专注。采用结构化的诊断过程来指导检查可以节省时间和避免挫折感。最重要的是,它 很有用。下次你再陷入bug之中时,那么不妨试试抛开键盘,将步骤一步一步写到白板上,像一个医生诊病一样进行调试。