Niklas Frykholm:分享程序员清理糟糕代码的8点建议

你猜怎么着?你刚接手一批糟糕的代码。恭喜你!现在这些活都是你的了。

糟糕的代码可能来源于任何地方。中介软件、网络、甚至在你自己的公司。

记着,那是某人几年前写的模块,现在人已离开公司。20个人经手那个模块,编辑、打补丁和修补漏洞,没有人真正理解他们在做什么。

你下载的开源代码怎么样呢?你知道那也是很可怕的,但它能解决燃眉之急,如果让你自己解决,可能要花好几年。

糟糕的代码不一定会成为难题,只要它没有产生太大的麻烦,也没有气得某人吐血。不幸的是,那种安乐状态极少能持续很久。你发现了一个新漏洞,你需要新的功能,你要进军新的平台。现在,你必须清理这团混乱。本文提供了一些实用的建议,帮助你摆脱不幸的处境。

bad code(from quickmeme.com)

bad code(from quickmeme.com)

这值得吗?

你要问你自己的第一个问题是,这段代码值不值得清理。我的观点是,你要在“是”和“否”之间权衡。如果你选择“是”的话,你要全权负责这段代码和重写编写,直到你完成某些让你感到高兴、乐意收进代码库的东西。

如果你选择“否”的话,你会认为即使那段代码看起来很糟,也不值得从你紧张的工作计划表中挤出时间修改它。所以你只是做了能解决当前问题的最小的调整。

换句话说,这取决于你把这段代码看成是你自己的还是别人的。

这两种选择各有优点。优秀程序员看到糟糕的代码会感到心里发痒。他们举着火把,拿着铁叉,高喊着:“这里没清理!那里没清理!”这是一个好习惯。

但清理代码也是一项浩大的工程。我们很容易低估这项工作所需的时间。你所耗费的时间可能像将这段代码重新写过一样,并且短期内看不到成效。花两周清理代码不会给游戏增加任何新功能,却可能给你带来新问题。

另一方面,从长远看来,不清理代码的影响可能是是灾难性的。信息熵是代码杀手。

所以,从来就没有容易的做法。你要考虑的事有:

*你希望对代码做多少调整?只是一个需要修复的小漏洞还是希望返回多次调整修改以增加新功能的代码。如果是前者,那么也许最好是睁一只眼闭一只眼。然而,如果是一整个需要你费大量时间处理的模块,那么你最好马上花时间清理它,以免日后头疼。

*你是否需要/想将修改任务交给前一级开发部门?这是一个正在开发的开源代码的项目?如果是,且你想将修改任务交给前一阶段,那么你不能对代码做出任何大改变,否则你的每一次调整都会把你推入地狱。所以,做个好队友,将漏洞的补丁发给维护人员吧。

*清理代码的工作量是多少?你一天可以实际可以清理多少行代码?根据数量级估算,不少于100不多于1000,所以我们假设是1000。所以如果这个模块有30000条代码,你需要花一个月的时间。你挤得出这些时间吗?值得花这些时间吗?

*这段代码是不是核心功能的一部分?如果这个模块的作用是次要的,比如渲染字体或载入图片,你可能不会关心它是否混乱。你之后可能会用其他东西将整个模块转换出来。但你应该让代码与核心功能相关。

*这段代码到底有多糟?如果只是有一点点乱,那么你也许可以无视它。如果确实乱得令人发指,那么你必须清理它。

1、测试用例

严格地清理一段代码意味着为它耗费大量时间。

如果你有一个体面的、覆盖性良好的测试用例,你可以马上知道什么地方错了,然后很快想出你犯了什么愚蠢的错误。如果没有测试用例,耗费时间和精力清理代码的过程是很可笑的。所以,要有一个测试用例,这是你首先要做的事。

单元测试最好,但并非所有代码都适合单元测试。如果单元测试太麻烦,可以改用综合测试。例如,开启游戏关卡,让角色执行与正在清理的代码有关的一套动作。

因为这种测试很费时间,所以不可能在每一次调整后都做一次。但如果你将每一次修改都置于源代码管理之下,就不算太坏。每过一段时间就测试一次(如,每做完5次调整)。当发现问题时,你可以对这些调整采用折半查找法,找出哪个修改引起问题。

如果你发现没有被测试检验出来的问题,那么你要确保你将问题添加到测试中,过后你才有可能改正它。

2、使用源代码控制

仍然必须有人告诉你要使用源代码控制吗?我希望不用。

对于清理工作,源代码控制是非常重要的。你会对一段代码做很多很多小调整。如果代码崩坏了,你肯定希望能在修改历史中查看,然后找出到底是哪里出错了。

另外,如果你也和我一样,那么有时候你会启动代码重构路径(如移除一个类),之后又意识到这不是一个好主意,或者这是一个好主意,但如果你先做点其他什么事,一切都会简单得多。所以你希望能够快速复原代码,先做其他什么事。

你的公司应该有一个源代码控制系统,允许你单独做修改,然后提交,而不必烦扰到其他人。

但即使没有,你也应该使用源代码控制。所以,下载Mercurial(或Git),创建一个新的资源库,将你用公司的系统查找出来的代码放进去。在那个资源库中做你的修改工作,然后交付。当你完成后,你可以将所有修改合并入那个系统。

将那个资源库复制到敏感的源代码控制系统中,只需要几分钟的时间。这样是绝对值得的。如果你不了解Mercurial,那就花一个小时学习吧。你会为自己的付出感到高兴的。或者如果你偏好Git,那就花30个小时学习吧。(我是开玩笑的,大家都不傻。)

3、一次只做一个(小)修改

处理烂代码的方法有两个:变革和改良。变革法就是将所有东西都删掉,从头再写一次。改良法就是在不破坏代码的情况下,每次做一个小的修改,最终重构代码。

本文要讲的是改良法。我不是说变革法永远不必要。有时候,代码实在太糟了,只能重新写过。但对改良的缓慢感到沮丧而主张变革的人往往没有意识到整个问题的复杂性,因此不够信任当前的代码。

改良代码的最好方法是一次只做最小的修改,然后测试,提交。当修改很小时,我们很容易知道它的修改效果,确保它不会影响当前功能。如果出错了,你只需要检查少量代码。

如果你开始做修改时发现不对劲,你还会以损失太多时间,只要返回上一次提交就可以了。如果过了一段时间你发现出现了微小的错误,使用折半查找法就可以发现导致错误的小修改。

我们常犯的错误是,同时做多件事。例如,在除去不必要的代码后,你可能会注意到API并不像你所希望的那样彼此正交,所以你开始重新安排。不要!首先除去继承的代码,提交,然后修改API。

聪明的程序员就是这么组织工作的,实际上他们不必太聪明。

要将现有的代码一点一点地往你所希望的样子调整。例如,在一个小步骤中,你可能会重新命名。在下一个小步骤中,你可能会将某些成员变量改成功能参数。然后你重新记录某些算法,好让它们更加清楚。等等。

如果你做修改时发现它是一个比你原来想象的还要庞大的调整,不要害怕复原代码,要将相同的过程分成更小、更简单的步骤。

4、不要同时清理和修改

这是第3条的推论,但因为比较重要,所以单独列成一个点。

这是一个普遍问题。因为你想增加某些新功能,你开始查看模块。然后你注意到这段代码组织得不好,所以你开始重新组织,同时,你还添加了新功能。

这个做法的问题是,清理和修改存在正好相反的目标。当你清理时,你想让代码看起来更好,同时不改变它的功能。当你修改时,你想把它的功能改成更好的那一种。如果你同时清理和修改,要保证你的清理不会改变功能是很困难的。

所以,先清理。等你有了一个漂亮的代码基础后再添加新功能。

5、移除所有当前不需要的功能

清理所需的时间与代码的数量、复杂度和混乱度成正比。

如果代码中存在当前不需要或在可预见的未来内不使用的功能,那就删除它。这么做,一方面减少了你要查看的代码数量,另一方面也让代码结构更清楚(通过删除不必要的概念和相关性)。这样就你可以更快地完成清理工作,最终结果也会更简单。

不要因为“可能哪天就用上了”就保留代码。代码是高投入的——需要移植、检验、读取和理解。代码越少越好。即使哪天你确实需要这些旧代码,你总是可以源代码库中找到它。

6、删除大部分注解

糟糕的代码几乎没有好注解。相反的,它们往往是:

// Pointless:

// Set x to 3

x = 3;

// Incomprehensible:

// Fix for CB (aug)

pos += vector3(0, -0.007, 0);

// Sowing fear and doubt:

// Really we shouldn’t be doing this

t = get_latest_time();

// Downright lying:

// p cannot be NULL here

p->set_speed(0.7);

阅读这段代码。如果注解对你来说没意义,或根本不能帮助你理解代码——那就果断删除。否则,在之后阅读代码时,你会浪费精力理解这些注解。

有注解的死代码也是一样的。果断删除。如果有需要,你也可以在源代码库中找。

甚至当注解正确和有用时,记着你要对代码所做的是大量重构的工作。当你完成修改后,那些注解可能就不再正确了。这个世界上不存在什么单元测试能告诉你“注解错误”。

好的代码需要的注解极少,因为代码本身就结构清楚、容易理解。命名恰当的变量不需要注解来解释它们的目的。输入和输出清楚、无特例的功能也不需要注解。简单、编写良好的算好不需要注解、声明文件、前提条件就可以理解。

在许多情况下,最好删除旧的注解,专心致志地将代码写清楚、然后返回添加必须的注解——现在反思新的API和你自己对代码的理解吧。

7、删除共享可变状态(游戏邦注:这是指在整个应用程序中共享且随时可变的变量)

说到理解代码,共享可变状态是一个大问题,因为它会产生可怕的“远距离活动”,即一段代码的修改,彻底改变了另一段代码的功能。我们经常说多线程是很复杂的。但事实上,共享可变状态的线程才是问题。如果你删除了共享可变状态,多线程也没那么复杂了。

因为你的目标是编写高性能的软件,你不可能删掉所有可变状态,但减少可变状态仍然可以大大优化代码。努力做“几乎功能性的”程序,确保你知道你在哪里、为什么、改变了什么状态。

共享可变状态可能在以下几个地方产生:

*全局变量。这是典型的例子。现在,所有人都肯定全局变量是不好的。但注意(这是有时候人们不能区别的地方),只有共享可变状态才是问题。全局变量没有那么糟。Pi不坏,Sprintf也不差。

*对象。对象是实现大量功能(方法)的方式,暗含了很多可变状态(类成员)。如果懒惰的程序员必须在不同方法之间通过某些信息,他会做一个自己可以随意读取和写入的新类成员。这几乎就像是全局变量。对象具有的成员越多,方法越多,问题越大。

*宏函数。你应该听说过吧。这些传说中的生物居住在最黑暗的代码库的最深处。郁闷的程序员在昏暗的酒吧里发泄他们的烦恼:“我翻了又翻,简直不敢相信自己的眼睛,整整12000条啊。”当功能足够大,他们的局部变量几乎就跟全局变量一样糟。改变局部变量会产生什么影响,不可能预测出来。

*引用和指示参数。没有常量的引用和指示参数可以用作介于调用函数和被调用函数之间的共享可变状态。

如何处理共享可变状态,以下有几条实用的建议:

*将大功能分解成几个小功能。

*将大对象分解成小对对象,然后分群组。

*不通用类成员。

*将方法改成常量,返回结果而不是改变状态。

*将方法改成静态的,将它们的参数作为参量,而不是从共享状态中读取。

*彻底删除对象,将功能作为纯功能来执行,不带附加作用。

*将局部变量设为常量。

*将指示变量和引用参数改成常量。

8、减少不必要的复杂性

不必要的复杂性往往是过分设计的结果——支持结构(游戏邦注:如序列化、引用计数、虚拟化界面、抽象因素等)掩盖了执行实际功能的代码。

有时候产生过度编程是因为软件项目的启动带有过多目标,超过了实际上能完成的程度。我认为,这反映了阅读关于设计模式和瀑布模型的程序员的野心,他们认为过度编程会让产品更“可靠”、品质更高。

通常,沉重、死板、太复杂的模型不能满足功能需求,这是设计师始料未及的。这些功能之后会被当作漏洞、附加功能和程序后门来执行,结果是绝对指令的混淆和彻底的混乱。

对抗过分编程的方法是,只编写你知道我需要的东西。当你需要时才添加新的东西,而不是在之前就添加。

减少不必要的复杂性的建议如下:

*移除你目前不需要的功能。

*简化必要的概念,删除不必要的概念。

*移除不必要的抽象函数,替换成更精确的执行。

*移除不必要的虚拟化,简化对象层次结构。

*如果只有一个设置是需要的,那就避免在其他设置中运行模块。

via:游戏邦/gamerboom.com

感谢支持199IT
我们致力为中国互联网研究和咨询及IT行业数据专业人员和决策者提供一个数据共享平台。

要继续访问我们的网站,只需关闭您的广告拦截器并刷新页面。
滚动到顶部