让问题永不重现
作者:Max Kanat-Alexander
发布日期:2013年11月19日
在代码库中解决问题时,症状消失并不意味着工作完成。只有当问题彻底消失且永不复现时,才算真正解决。
当问题不再有可见症状时,很容易停止解决工作。你修复了漏洞,没人抱怨,似乎还有其他紧迫问题。为什么要继续投入?现在看起来没问题,对吧?
不对。请记住,软件中最重要的是未来。软件公司陷入代码库无法管理境地的原因,正是没有彻底解决问题。
这也解释了为什么一些组织无法将混乱的代码库恢复良好状态。他们看到代码中的一个问题,处理到没人抱怨就转向下一个症状。他们没有建立框架确保问题永不复发,没有追踪问题根源并彻底消除。因此,代码库永远无法真正“健康”。
这种未能完全处理问题的模式非常普遍。结果,许多开发者认为大型软件项目无法保持良好设计——他们说:“所有软件最终都必须扔掉重写。”
这不正确。我职业生涯大部分时间要么从头设计可持续代码库,要么将糟糕代码库重构为良好状态。无论代码库多糟糕,你都能解决问题。但你必须理解软件设计,需要足够人力,并且必须处理问题直到它们永不复发。
一般来说,问题解决程度的良好指导原则是:问题解决到无需任何人再关注的程度。
绝对意义上实现这一点不可能——你无法预测整个未来,等等——但这更多是哲学上的反对而非实际障碍。在大多数实际情况下,你可以有效解决问题,使现在无人需要关注,且没有明显理由未来需要关注。
示例
假设你有一个网页,编写了统计访问量的“点击计数器”。你发现计数器漏洞——它统计的访问量是实际的1.5倍。你有几种解决方式:
-
忽略问题
理由是网站不热门,计数器不准没关系。而且它让网站看起来更成功,可能对你有帮助。
这是糟糕解决方案,因为未来许多场景中这可能再次成为问题——尤其当网站非常成功时。例如,主流新闻发布你的点击数据——但数据是假的。这导致丑闻,用户失去信任(毕竟你知道问题却没解决),网站再次不受欢迎。很容易想象其他方式让问题回来困扰你。 -
快速hack解决方案
显示点击量时除以1.5,数字就准确了。但你没调查根本原因,结果发现它在早上8:00到11:00统计3倍点击量。后来流量模式变化,计数器再次完全错误。你可能一段时间没注意到,因为hack让调试更困难。 -
调查并解决根本原因
你发现它在8:00到11:00统计3倍点击量。发现这是因为Web服务器在那段时间删除磁盘上许多旧文件,这以某种方式干扰了点击计数器。
此时你有另一个hack解决方案机会——可以禁用删除过程或降低运行频率。但这没真正追踪根本原因。你想知道的是:“为什么机器上发生其他事情会导致计数错误?”
进一步调查,你发现如果中断程序然后重启,它会重新统计最后一次访问。删除过程占用机器太多资源,导致8:00到11:00每次访问中断计数器两次。所以那段时间每次访问统计三次。但实际上,根据机器负载,漏洞可能添加无限(或至少不可预测)计数。
你重新设计计数器,使其即使中断也能可靠计数,问题消失。
显然列表中正确选择是调查并解决根本原因。这使问题消失,大多数开发者认为到此完成。但如果你真想确保问题不再需要人工关注,还有更多工作。
首先,有人可能修改点击计数器代码,将来恢复为损坏状态。显然正确解决方案是添加自动化测试,确保点击计数器即使中断也能正确运行。然后确保测试持续运行并在失败时提醒开发者。现在完成了吧?
不。即使此时,还有一些未来风险需要处理。
下一个问题是编写的测试必须易于维护。如果测试难以维护——开发者修改代码时测试经常变化,测试代码本身晦涩,代码变化时容易返回误报等——那么测试很可能将来损坏或被人禁用。然后问题可能再次需要人工关注。所以你必须确保编写了可维护测试,如果不可维护就重构测试。这可能引导你调查测试框架或被测系统,找出使测试代码更简单的重构方法。
此后你还要关注持续集成系统(测试运行器)——它可靠吗?会失败导致测试需要人工关注吗?这可能是另一条调查路径。
所有这些调查路径可能发现其他问题,然后必须追踪其根源,可能发现更多问题需要追踪,等等。你可能发现,只需从一些症状开始,非常坚定地追踪根本原因,就能发现(可能解决)代码库所有主要问题。
有人真的这样做吗?是的。起初可能困难,但随着解决越来越多根本问题,事情确实开始变容易,你能越来越快前进,问题越来越少。
深入兔子洞
除此之外,如果你真想冒险,还可以问一个问题:为什么开发者最初编写了有漏洞代码?为什么漏洞可能存在?是开发者教育问题吗?是他们的流程问题吗?他们应该边写代码边写测试吗?系统中有设计问题导致难以修改吗?编程语言太复杂吗?使用的库写得不好吗?操作系统行为不良吗?文档不清晰吗?
一旦得到答案,你可以问那个问题的根本原因是什么,继续问直到满意。但注意:这可能带你进入兔子洞,改变你对软件开发的整体看法。实际上,理论上这个系统无限,最终会解决整个软件行业的根本问题。你想走多远取决于你自己。
评论
Jeff Dickey 2013年11月20日上午9:26
做了几年TDD后,我发现它迫使我一直这样思考(回归测试成为遥远记忆)
好文章!喜欢点击计数器示例!
回复:Max Kanat-Alexander 2013年12月8日下午9:57
谢谢,Jeff!TDD对你起到这作用太棒了。我认为TDD不总是固有解决的一个方面是 creeping complexity——理论上仍然可能创建相当复杂的野兽,恰好测试良好。(不过测试[如果本身写得好]倾向于帮助重构,使问题更容易修复。)
-Max
Bug fixing is really important, it isn’t? | A thousand programming languages 2015年4月8日上午11:56
[…] 不仅如此… 还说当你解决问题时,你应该让它永不复发! […]
The Fundamental Philosophy of Debugging - Best Application Development Platforms, Software, App Developers 2017年8月16日上午4:18
[…] 但人们做了不那么愚蠢的版本,没修复漏洞。他们没有深入问题基本原因,而是用一些变通方法掩盖漏洞,这些方法永远留在代码库中[…]
Mike W. 2017年9月1日下午8:37
好例子;非常好地说明了观点。应注意示例没完全追踪到底。为什么点击计数器中断重启后重新统计最后一次访问?对于每个shell脚本漏洞,“深入兔子洞”部分描述了问题。非常非常少写shell脚本的人真正理解shell。他们从其他作者那里复制shell脚本片段,其他作者基于其他片段编写,那些片段由也不理解shell的人编写。https://unix.stackexchange.com/q/131766/135943 是澄清至少最常见误解的好起点,但关键是“漏洞”实际上是广泛缺乏正确教育和正确shell脚本示例。
完全追踪漏洞后看到它消失真是太棒了。
回复:Max Kanat-Alexander 2017年9月2日下午7:47
它追踪到底了。重新设计甚至消除了问题。也就是说,你无需问问题,因为被问对象不再存在。示例可能缺少一些完全回答你问题的细节(并显示它实际上已回答),这更多在 http://www.codesimplicity.com/post/the-fundamental-philosophy-of-debugging/ 中涵盖。
从我角度看,shell脚本的实际兔子洞是语言设计方式对普通用户难以理解、不直观且令人惊讶。它与大多数其他编程语言不一致。
但从实际角度,因为我们不是来修复bash的,确实可以追踪到程序员理解失败。事实上,我认为所有编程失败都可追踪到理解、责任或意识问题。这可能是我未来博客的主题。
-Max
Mike W. 2018年11月28日下午4:31
我最近看到 https://www.fastcompany.com/28121/they-write-right-stuff,是你“深入兔子洞”部分的绝佳例子。它描述了Lockheed-Martin团队如何处理火箭飞行软件代码准确性。当他们发现漏洞时,不仅修复它;还检查流程如何允许漏洞首先出现,寻找相同流程缺陷可能允许的所有潜在漏洞,修复流程,修复所有漏洞等。
Make It Never Come Back – coding | factory 2018年12月22日下午6:49
[…] 更多 […]
The Fundamental Philosophy of Debugging - Komunitas 101 News 2019年2月22日上午1:49
[…] 但人们做了不那么愚蠢的版本,没修复漏洞。他们没有深入问题基本原因,而是用一些变通方法掩盖漏洞,这些方法永远留在代码库中[…]