如何编写高质量的程序

学习任何编程语言都会有一个基本的过程,开始的时候学习基本的语法,然后学习各种库,框架,开始做各种项目。在做项目的过程中,随着代码量的增加,我们会渐渐感到失去对程序的掌控能力,bug开始增加,牵一发而动全身,顾此失彼。这充分说明了编写高质量程序的重要性,这里的“高质量”主要指程序的正确性,可读性,可维护性。

什么是高质量的程序

正确性

程序正确性的重要程度无需多言,尤其在一些特殊领域,例如芯片制造业,航天业,武器制造业,对程序正确性往往有着极其严格的要求,因为一旦程序出错,代价往往是巨大的。在这些领域,需要使用形式化方法(formal methods)来自动验证程序的正确性,也就是说你需要证明程序的正确性,而不仅仅保证程序在大多数情况下是正确的。在其它领域,对正确性没有这么高要求,形式化方法也不适用,但是我们还是需要使用其它手段,例如测试,code review等等来保证软件的正确性。

可读性

可读性可以帮助程序作者理清思路,思路清晰后,程序不容易出错。另外,其它程序员在维护你的代码时,更容易理解你的意思,方便修改bug,方便扩展。

不要浪费自己的时间,更不要浪费别人的时间。

可维护性

这里的可维护性主要指程序应对变化的能力。程序在完成基本功能后,可能会发生各种改变:用户需求变了,性能达不到要求需要重新实现算法,等等。一旦程序的一个点发生改变,其它点如果也需要同时手动改变,那么程序会变的不可控制,出bug的机会会增加。想像一下,我们的程序是一个盒子,在添加新功能时,如果只需要把新模块插到一个地方,新模块就可以被系统使用,这样的程序可维护性是很高的。但是如果添加新功能时,需要把原来的程序盒子拆开,其它模块也需要相应修改,才能加入新模块,这样的程序可维护性就很差。

提高程序质量的重要措施

测试

为什么强调先编写测试用例,再实现程序?先编写测试用例的意义在于,让编写程序的人对程序本身有更好的理解。因为你首先得明白什么样的程序是正确的,然后才能写出正确的程序。测试用例其实是对程序正确性的一种描述。

为什么强调自动化测试,而不是手动测试?因为自动化测试可以增加测试的便捷度,而人们通常会更多地使用那些便捷度高的东西。我在做个人项目的时候就发现,在编写了自动测试的脚本后,我每改动一点程序,就会自动运行一下脚本,在此之前,我明知道测试很重要,但是还是不会测试的如此频繁。这样的好处是可以方便定位bug,否则在系统经过了大量改动之后,出了bug都不知道可能在哪里。

在对程序进行重构时,很重要的一点就在于,一定要先写好测试用例,然后每改动一点,就自动测试一下,保证程序始终保持在可控状态。

良好的编程风格

良好的编程风格,可以增强程序的可读性,一个结构清晰的程序,你会更容易从中发现错误。另一方面,当程序发生变化时,很可能引入新的bug,良好的编程风格可以减少这种bug的出现。下面是与编程风格相关的一些措施。

找一份你使用的编程语言的风格指南,例如Google的编程语言风格指南系列,Python的PEP8,并一直遵守这份指南的内容,如果有自动化工具帮助你保持这种风格,那再好不过。

寻找你所使用语言的最佳实践,他们可读性强,经过了大量实践的考验,被广泛接受,所以尽可能多地使用他们。

变量,函数名,类名,都需要一个好名字。程序本身是对解决方案的一种描述,一个好的名字会增强这种描述性,也会让你的思维集中于解决方案,同时让其它人更容易理解你的解决方案。

在程序中直接使用的常量,一般被称为 Magic Numbers, 一方面它不利于其它程序员对程序的理解,因为没有人知道这个常量代表什么。另一方面,多个常量之间可能是有关系的,直接使用常量根本反应不出这种关系。

首先这种做法降低了可读性,一个变量前面一个含义,后面一个含义,这会给阅读程序的人带来困扰。

尽量减少变量定义的点与变量最后一次使用的点之间的跨度,这样可以使变量与其相关代码变得紧凑,提高可读性,不用在使用变量时再去很多的地方查看其它引用。

过长的函数会让读者陷入细节的泥潭,还需要前后来回看才能明白前面一大段和后面一大段代码的关系。将函数分解,然后给函数起一个好名字,读者马上就能明白这段代码在做什么。

提高应变能力

程序应对变化的能力强,可扩展性就强,也更容易在变化时保证正确性,这样的程序可维护性强。下面是一些提高程序应变能力的措施。

不要使用常量的另一个原因在于常量可能变化,如果程序中多次引入了这个常量,那么一旦这个常量要发生变化,就需要同时改动许多地方,这时候,如果有些地方没有改,就会使程序不一致,可能引入bug。

同一变量名不要有多种含义另一个原因在于,多种含义之间可能会相互影响,第一次写程序时你可能记得这些影响,但是以后对程序进行改动的时候,你可能就忘记了。例如函数内一段代码执行后,索引i 的值等于一个长度,但是这段代码后,你没有将i赋值给另一个变量len,而是直接使用它。等过一段时间后,你或者其它人修改这段程序时,很可能忘了这段代码执行后i的值需要等于一个长度,因为这是一种隐式的约定,所以很容易被忽视。

保证变量作用域小也有利于重构。当一个函数变得很长时,你可能需要将它分解成多个函数,这时候,如果变量跨度小,就可以很方便地提取函数,不用来回查找与此函数相关的变量的引用。

如果有一段代码在很多地方重复,这就告诉你,需要把他们提取成一个函数。因为代码的重复意味着这是一块独立的逻辑,独立的逻辑可以抽象成一个函数。另一方面,一旦这段逻辑需要发生变化,只需要修改这个函数就可以了,不需要把所有地方都手动修改一遍。

数据驱动的意思是用数据表示来代替程序逻辑。例如,我们需要一个程序,判断某个月有几天,在实现时,最好用一个数组表示各个月的天数,需要哪个月直接查询就好,而不要使用大量的if语句来作逻辑判断。这只是一个小例子,它提醒我们,如果程序中含有大量判断语句,就应该想一想,能不能用数据来驱动逻辑,这样需要修改的时候,我们直接修改数据就好,而不用修改程序逻辑。

我曾经接手过一个项目,这个项目其实是一个工具集,根据用户的选择,调用不同的工具。原始的代码里,就使用了大量if语句,并且每个工具其实调用方式和代码都很相似。这样,我每次添加新工具时,就需要找到多个if语句块,作相应修改。如果用数据驱动的话,我们完全可以去掉这些if语句,在用户的选择与工具之间建立对应关系,这样每当新添加工具时,只需要把工具加到系统里,系统会根据这个表直接找到这个工具。这其实和之前举的盒子的例子很相似,添加新工具时,只需要把工具插到盒子上的槽上,根本不用打开盒子。这就大大提高了程序的可扩展性。

控制复杂度

要保证软件的高质量,很重要的一方面在于控制复杂度。控制复杂度的一个很重要的手段在于分解复杂的事物。我们之所以觉得一个事物复杂,是因为同一时间需要关心的事情太多,把复杂事物分解后,每次我们只需要关心很少的事情,这样就控制住了复杂度。

如果一个函数或类过大,他们会变得过分复杂,你同一时间需要关心许多细节。将函数或类变小之后,你的思维在一段时间内可以集中在同一个抽象层次,而不必过于深入其细节,这样更容易发现程序中的缺陷,因为你每次只需要关心很少的事情。在最高层,你只需要关心模块之间的关系,关心算法的流程,不必关心模块内部的事情。在最低层,你只需要关心一个模块内部的事情,而不必关心其它事情。

函数参数过多可能说明这个函数负责了太多的事情,你需要将这个函数分解。另一方面,你需要从逻辑上考虑,这些参数是不是一个整体,如果是一个整体,那么直接传过来一个结构体,或者传过来一个对象,是不是更合适?

如果一个函数或类被分解为过多的抽象层次,在模块内部,你确实只需要关心很小的事情,但是这时候,由于模块过多,抽象层次过深,他们之间的关系又使复杂度增长起来。

使用自动化工具

自动化工具迫使我们养成良好的编程习惯,而且不容易出错。再次强调:

    工具越是使用方便,你越会频繁使用它。

所以,尽可能地让你的工具使用便捷。 例如,使用一些静态检测工具在编辑时自动帮助你检测程序的不良风格;使用重构工具帮助你重构;使用自动化测试工具在保存时自动运行测试等等。

注意事项

没有什么事情是一成不变的,所有的法则都需要考虑具体的情况。如果你要用一个法则,需要真正明白自己为什么要用,需要去权衡,而不要为了能用上这个法则而生搬硬套。

好好问问自己:

参考资料

这篇文章是我这段时间阅读过一些书后的想法,书目有

在阅读这些书的同时,我还在维护其它人的代码,做自己的个人项目。在阅读的过程中,我会不断地想到我做的项目哪里有问题,可以用书中提到的方法去修改,因此印象深刻。这些书单纯读也非常有好处,但是如果可以结合到自己的项目中,会有更大裨益。因为只有产生了强烈的共鸣,才能保证真正理解了一个东西。

上面提到的一些措施,都是我遇到过的,所以印象比较深刻,这几本书中还有大量提高程序质量的方法,我这里只是一个引子,希望给有心人打开一扇窗户。

comments powered by Disqus