1.2 代码质量目标
如果我们购买一辆轿车,质量或许是我们考虑的主要因素之一。我们希望这辆轿车:
● 安全;
● 能正常使用;
● 不出故障;
● 表现可以预测——当我们踩下制动器,车子应该慢下来。
如果我们问某人,是什么造就了一辆高质量的汽车?最可能得到的答案就是“精良的制造”。这意味着优秀的设计,在投产前进行安全性和可靠性测试,并正确组装。软件也大抵如此:要制作出高质量的软件,就必须确保它“制造精良”。这就是代码质量的全部含义。
“代码质量”这个词有时可能会让人联想到,对一些不重要的琐事提出的近乎吹毛求疵的建议。毫无疑问,你时常会遇到这种建议,但它们并不是代码质量的真正含义。代码质量很大程度上出于对实际情况的考量,它有时与细节有关,有时又关乎大局,但目标都是一样的:创造更好的软件。
尽管如此,代码质量仍然是我们很难确切定义的一个概念。有时候,我们看到某些代码时可能会想“看起来很不舒服”,其他时候则可能偶然看到一段“看起来非常棒”的代码。代码为何引起这种反应,原因并不总是一目了然,有时候只是我们的本能反应,并没有确切的理由。
定义代码质量高低,本来就是主观的,更多的是出于判断。为了做出更客观的评判,我个人认为有益的做法是后退一步,考虑一下编写代码时真正试图实现的目标。在我看来,帮助我实现这些目标的代码就是高质量的,而产生阻碍作用的代码就是低质量的。
我在编写代码的时候要实现的4个高层目标如下:
● 代码应该正常工作;
● 代码应该持续正常工作;
● 代码应该适应不断变化的需求;
● 代码不应该重复别人做过的工作。
后面的内容将更加详细地介绍这4个目标。
1.2.1 代码应该正常工作
这个目标显而易见,或许不需要说明,但我无论如何都要做一番解释。我们编写代码的目的是试图解决某个问题,例如实现某个功能、修复缺陷或执行某项任务。代码的主要目标是能够正常工作——它应该解决我们打算让它解决的问题。这意味着,代码是没有缺陷的,因为缺陷很可能阻止代码正常工作和全面解决问题。
确定代码“正常工作”的含义是,我们必须了解所有需求。例如,如果我们要解决的问题对性能(如延迟和CPU占用率)很敏感,确保代码有合适的性能就应该归入“正常工作”的范畴,因为这是需求的一部分。用户隐私和安全性等其他重要考虑因素也适用这一原则。
1.2.2 代码应该持续正常工作
代码的工作可能非常短暂。今天,它可能正常工作,但我们如何确保明天或者一年之内它都能正常工作?这样的担心看起来好像莫名其妙,为什么代码会突然停止工作?要点在于,代码并不是与世隔绝的,如果我们不多加小心,它很容易因为周围事物的变化而崩溃。
● 代码很可能依赖于其他代码,而这些代码会被修改、更新和更换。
● 任何新功能需求都意味着要对代码进行修改。
● 我们试图解决的问题也可能随时间的推移而发展:消费者的偏好、业务需求和技术考虑都可能变化。
如果代码在今天能够正常工作,明天却因为上述因素变化而出现问题,那么它没有太大的用处。创建当下可以正常工作的代码往往很容易,但创建一直能正常工作的代码就要难得多。确保代码持续工作是软件工程师面对的问题之一,也是在编程各阶段都要考虑的问题。以事后诸葛亮的眼光去考虑它,或者认为只要以后增加一些测试就能实现这个目标,往往都不是很有效。
1.2.3 代码应该适应不断变化的需求
很少有只编写一次、永远不用再修改的代码。一款软件的持续开发可能跨越几个月、几年,甚至几十年。在整个过程中,需求都在改变:
● 业务状况变化;
● 消费者偏好变化;
● 设想失效;
● 新功能持续增加。
决定在代码适应性上投入多少精力,可能是很难权衡的问题。一方面,我们深知软件需求将随时间推移而发展(极少看到反例)。另一方面,我们往往不能确定它们究竟会如何发展。对一段代码或者一款软件,几乎不可能准确预测出它在以后的一段时间内将如何变化。然而,我们不能仅因为不能确定软件如何发展,就完全忽视软件将会发展的事实。为了说明这一点,我们来考虑两种极端的情况。
● 方案 A——我们试图准确预测未来的需求可能会如何演变,并设计支持所有潜在变化的代码。我们可能要花几天或者几周的时间来描绘出代码和软件所有可能的演变路径。然后,我们必须小心翼翼地考虑缩写代码的每个细节,确保它支持所有未来可能出现的需求。这将严重拖慢我们的工作,本来3个月就可以完成的软件,现在可能要花上一年甚至更长的时间。最终,这些时间也可能是浪费的,因为竞争对手将比我们提前几个月进入市场,我们对未来的预测很可能完全是错误的。
● 方案 B——我们完全无视需求可能演变的事实。我们编写恰好满足现行需求的代码,不在代码适应性上做任何努力。软件中到处都是不可靠的假设,各个子问题的解决方案都捆绑在一起,成为一大堆无法区分的代码。我们在3个月内就投放了软件的第一个版本,但初始用户的反馈说明,如果我们想要取得成功,就必须改良其中的一些功能,并添加新功能。对需求的改变并不大,但因为我们编写代码时没有考虑适应性,唯一的选择就是扔掉全部代码,从头再来一遍。我们必须再花3个月重写软件,如果需求再次改变,此后还得再花3个月重写。到我们完成满足用户需求的软件时,竞争对手再一次打败了我们。
方案A和方案B是两个极端,两者的结果都很不好,它们也都不是制作软件的有效方法。相反,我们需要找到一种介于两个极端的方法。从方案A到方案B的整个谱系中,哪里才是最优的并没有唯一的答案,这取决于我们所开发的项目,以及我们所在单位的文化。
幸运的是,我们可以采用一些普适技术,在不确定未来变化的情况下确保代码的适应性。本书将介绍许多此类技术。
1.2.4 代码不应该重复别人做过的工作
在我们编写代码解决问题时,通常会将一个大问题分解为多个较小的子问题。例如,我们打算编写加载图像文件,将其转换为灰阶图像,然后保存的代码,那么需要解决的子问题如下:
● 从文件中加载一些数据;
● 将这些数据解析为某种图像格式;
● 将图像转换为灰阶图像;
● 将图像转换为数据;
● 将数据存回文件。
这些问题中的许多已被其他人解决,例如,从文件中加载一些数据可能是由编程语言内置方法完成的。我们不用自己编写与文件系统进行低层通信的代码。同样,我们也许可以从现有的库中调用代码,将数据解析为图像。
如果我们自己编写与文件系统进行低层通信的代码,或者将一些数据解析为图像,实际上就是在重复别人做过的工作。最好的方式是利用现有解决方案而不是重写一遍。这样做的理由有多个方面。
● 节约时间和精力——如果我们利用编程语言内置方法加载文件,可能只要几行代码、花费几分钟。相反,自己编写代码完成这一工作可能需要阅读许多关于文件系统的标准文档,编写成千上万行代码。我们可能要几天甚至几周的时间才能完成这项工作。
● 降低出现程序缺陷的可能性——如果现有的代码能解决指定问题,它应该已经全面测试过。这些代码很有可能已在外界使用过,因此代码包含缺陷的可能性已经降低,即使有缺陷,人们可能已经发现并修复过。
● 利用现有专业知识——维护图像解析代码的团队很可能是由图像编程专业人士组成的。如果JPEG编程技术出现了新版本,他们很可能对此十分了解,并更新相应的代码。通过重用他们的代码,我们就可以从他们的专业能力和未来的更新中获益。
● 使代码更容易理解——如果完成某项工作有标准化的方法,我们就有理由认为,其他工程师此前也知道这种方法。大部工程师可能在某个时间阅读了一份文件,立刻意识到完成这项工作的(编程语言内置)方法,并理解这种方法的功能。如果我们编写自定义逻辑来完成工作,其他工程师就不会熟悉,不能一下子就知道它的功能。
“不应该重复别人做过的工作”这一概念在两个方向上都适用。如果其他工程师已编写了解决某个子问题的代码,那么我们应该调用这些代码,而不是自己编写代码来解决。同样,如果我们编写了解决一个子问题的代码,那么应该以某种方法构造代码,以便其他工程师能轻松地重用它们,而无须重复工作。
由于同一类子问题往往反复出现,因此人们很快就会意识到在不同工程师和团队之间共享代码的好处。