16-确保它不会再发生

125 阅读9分钟

当你在解决代码中的问题时,你不应该止步于只修复问题表象。而是应该确保问题彻底消失并且永远不会再发生。开发者通常在修复完问题症状之后就认为完事大吉了。确实从某种意义上说你已经修复了bug,也没有人再抱怨了,另外还有更多积压的问题需要处理。所以为什么还要继续把精力花在已经修复完毕的问题上?现在一切都回归正常了不是吗?并非如此。

请记住,我们最在意的是软件的未来。

软件公司代码库之所以会陷入无法维护的失控局面,是因为他们并没有真的在解决问题,只有切实解决这些问题之后,代码的可维护性才可能好转。这也解释了为什么有的组织内部的紊乱代码始终无法回归到一个良好的可维护状态。当他们遇到一个问题时,他们应对问题的出发点仅仅是设法让提出问题的人停止抱怨,用这种态度解决问题之后继续以同样的态度应付下一个问题。他们不会考虑引入一个框架来阻止问题的再次发生。他们也不会追溯问题发生的根本原因然后斩草除根。所以他们的代码从来没有真正地“健康”过。

这种不打算从根本上解决问题的开发模式非常普遍。结果就是许多开发者相信大型项目根本就不可能保持终身良好的架构设计,他们常说:“所有的软件终将被遗弃并且被重写。”这种说法是错误的。我职业生涯的大部分时间要么是在从无到有地设计具有可拓展性的开发代码,要么在将糟糕代码库进行重构。无论代码库多么糟糕,你总能解决它其中的各种问题。但前提是你必须了解软件设计的相关原理,拥有足够的人力,以及务必以确保它们不会再次发生的态度解决问题。

总的来说,衡量一个问题是否被真的解决的恰当标准是:直到人们不需要再次对它进行关注。绝对地做到这一点是不可能的,因为你无法预测到所有的可能性,但这条原则更多的是想提供理论上的指引而不是实际的操作指南。在大部分实际情况中,你能做到的是当下不会再有人被这个问题困扰,但是并不代表问题在未来不会再次出现。

一个确保它不会再发生的例子

假设你个人拥有一个网页,你为这个站点编写一个用于统计用户访问量的“访问计数器”功能。不料你发现了这个访问计数器的一个bug:它的最终统计数会是实际数量的1.5倍。于是你有以下几个备选方案来解决这个问题:

  1. 你可以忽略这个问题。

这个方案的出发点在于你的站点反响平平,所以即使计数器统计出错了也没有什么大不了的。夸张的数字还能让你的站点看上去比实际上更成功,某种意义上也算是因祸得福。

而之所以这其实是一个糟糕的方案,是因为这个问题可能会在将来引起不必要麻烦——特别是在你的站点大获成功之后。例如一些主流出版商会公布你网站的用户访问量——但它们其实并非真实数据。这样的丑闻会让你的用户不再信任你(毕竟你一早就知道这个问题但并没有解决它),导致网站的运营又一落千丈。这只是这个问题引起的能想到的麻烦之一。

  1. 你可以绕过这个问题。

当你在展示访问量时,把最终数字除以1.5就好了。但是你并没有去探究引起问题的根本原因,这个未知原因可能又会导致上午8点至11点的访问量增长3倍。在将症状修复完毕一段时间后,可能流量的错误统计模式又会发生变化,导致统计数据再次出错。你可能不会轻易地注意到这个问题,因为那些绕过这个问题的代码会让排查真实原因的工作变得难上加难。

  1. 调查并彻底解决这个问题。

你发现之所以上午8点至11点的访问量会增加3倍,是因为服务器在那个时间段会删除许多旧文件,这个操作出于某些原因会对计数器的统计产生干扰。此时你又有了一个可以绕过问题的机会——你可以把删除文件任务禁用掉,或者减少它的执行频率。但并不算真的找到了问题发生的原因。你需要知道的是:“为什么开发机上看似风马牛不相及的一件事会导致计数器出错?”在经过更深入的调查之后,你发现如果中断计数程序然后将它重启,它会对最后一次访问再统计一遍。同时删除文件的操作占用了太多的机器资源,导致8点至11点的每一次用户访问都会引起计数器程序的两次中断。所以在那段时间内每一次的访问都被算了三遍。但实际上,根据开发机的负载不同,这个bug会让统计数字无上限递增(或者至少是不可预测的)。最终你对计数器重新进行了编码设计,以确保它在被中止后统计结果也是值得信赖的,问题便迎刃而解了。

很明显上述的可选方案中,最正确的办法是刨根问底找到根本原因然后解决它。大部分开发者都相信他们在工作中的确是按照这样的方式解决问题的。但是如果你想要确保问题不会再困扰大家,还有额外的任务需要完成。

首先人们有可能回过头对计数器程序代码进行修改,导致它又回到之前出问题的状态。很明显解决这个问题的最好办法是添加自动化测试,来确保即使程序被中止之后再次运行时功能依然是正常的。你还需要确保测试能够持续运行并且在运行失败时提醒开发者。这一步加上之后现在看上去就非常完美了,不是吗?并不是。就算完成了这一步,还有一些未来可能存在的风险没有被考虑到。

另一个问题是你编写的测试要易于维护。如果测试难以维护,比如当开发者在修改实现代码时,测试代码也需要大量修改的话,测试代码就不免显得过于晦涩了。这会导致容易把测试代码改坏并返回错误的测试结果——这样的测试极易失效或者是被人弃用。

问题可能会再一次出现在人们的视野里。所以请确保你编写的测试代码是具有可维护性的(可以参见第32章中的内容),或者重构测试代码让它变得具有可维护性。这会迫使你开始对测试框架或者是已经集成测试的系统做调研,思考如何才能将测试代码重构得更简单。

此时你可能又会对持续集成(测试执行工具)开始感兴趣:它可靠吗?当测试运行失败时能够引起人们的注意吗?这些问题需要在深入调研之后才能得到回答。在调研的过程中可能会引入其他需要追根溯源的问题,而这些问题又会继续引入更多有待回答的问题,并如此循环往复。你可能会发现从一个不经意的问题出发,然后锲而不舍地追溯下去,就能把整个代码库的大部分问题都挖掘出来(甚至解决完毕)。真的有人会这么做吗?有的。虽然说这项工作起步难,但随着你解决的底层问题越来越多,剩下的工作会变得轻松,余下的问题也能更为迅速地得到解决。

深入兔子洞

除此之外,如果你富有探索精神,还可以提出更多的问题:为什么开发者会写出错误代码?bug为什么会存在?是开发者接受的技能培训出了什么问题?还是工作的流程中存在纰漏?他们在编写代码的同时是否也应该编写测试?会不会是系统的设计缺陷导致代码难以修改?编程语言过于复杂了?他们用的类库编写的不够友好?操作系统出了什么问题?文档描述得不够清楚?

如果你有了关于某个问题的答案,你可以继续思考产生这个问题的根本原因又是什么,并且持续追问下去直到你所有的诱惑都已经解开。但是请小心:你并不知道这一串问题的终点在哪里,甚至整个过程会颠覆你对软件开发的看法。事实上从理论上来说,在这一套方法论下可以提出无限多的问题,并且终将让整个软件行业的根本问题得到解决。但是在这条路上要走多远还是取决于你自己。