12子程序的名字和长度
1. 子程序的名字
好的子程序名字能清晰地描述子程所做的一切。这里是有效地给子程序命名的一些指导原则。
1.1 描述子程序所做的所有事情
子程序的名字应当描述其所有的输出结果以及副作用。如果一个子程序的作用是计算报表总额并打开一个输出文件,那么把它命名为 ComputeReportTotals() 就不算完整。而 ComputeReportTotalAndOPenOutputFile() 是很完整。但是又太长且显得有点傻。如果你写的是有一些副作用的子程序,那就会起出很多又长又笨的名字。解决的方法不是使用某个描述性较弱的子程序名,而应该换一种方式编写程序,直接了当地解决问题而不产生副作用。
1.2 避免使用无意义、模糊或表述不清的动词
有些动词的含义非常灵活,可以延伸到涵盖几乎任何含义。像 HandleCalculation()、PerformServices()、OutputUser() 和 ProcessInput() 这样的子程序名根本不能说明子程是做什么的。最多就是告诉你这些子程序所做的事情与计算、服务、用户、输入有关。
有时一个子程序中仅有的问题就是其名字表达不清,而子程序本身也许设计的很好,但如果把它的名字由 HandleOutput() 改为 FormatAndPrintOutput(), 那你就很容易看清楚这个子程序的功能了。
还有另外一些情况,其中的动词之所以含糊,是由于子程序执行的操作就是含糊不清的。这种子程的问题在于目的不明确,而其模糊不清的名字仅是一种表象。如果是这种情况,那么最佳的解决办法便是重新组织该子程序及任何与之相关的子程序,以便使它们都具有更为明确的目的,进而赋予其能够精确描述这些目的的更为清晰地名字。
1.3 不要仅通过数字来形成不同的子程序名字
有个程序员把所有的代码都写成一个大的函数,然后为每 15 行代码创建一个函数,并把他们分别命名为 Part1、Part2 等。在此之后,他由创建了一个高层次的函数来调用这些部分(Partn)。这种创建子程序和给子程序命名的做法实在是骇人听闻。但程序员们有时会用数字来区分类似于 OutputUser、OutputUser1 和 OutputUser2这样的子程序。这些名字后面的数字无法显示出子程序所代表的抽象有何不同,因此这些子程序的命名也很糟糕。
1.4 根据需要确定子程序的长度
研究表明,变量名的最佳长度是 9 到 15 个字符。子程序通常比变量更为复杂,因此,好的子程序名字通常也会更长一些。另一方面,子程序名字通常是跟在对象名字之后,这实际上为其免费提供了一部分名字。总的来说,给子程序命名的重点是尽可能含义清晰,也就是说,子程序名的长度要是视该名字是否清晰易懂而定。
1.5 给函数命名时要对返回值有所描述
函数有返回值,因此,函数的命名要应该针对其返回值进行。比如说,cos()、customerId.Next()、printer.IsReady() 和 pen.CurrentColor() 都是不错的函数名,他它们精确地表述了该函数将要返回的结果。
1.6 给过程起名时使用语气强烈的动词家宾语的形式
一个具有功能内聚性的过程(procedure)通常是针对一个对象执行一种操作。过程的名字应该能反映该过程所做的事情,而一个针对某对象执行的操作就需要一个动词 + 宾语 形式的名字。如 PrintDocument()、CalcMonthlyRevenues()、CheckOrderInfo() 和 RepaginateDocument() 等,都是很不错的过程名。
在面向兑现语言中,你不用在过程名中加入对象的名字(宾语),因为对象本身已经包含在调用语句中了。你会用 document.Print()、orderInfo.Check()、monthlyRevenues.Calc() 等调用子程序。而诸如 document.PrintDocument() 这样的语句则显得太臃肿,并且当他们在派生类中被调用时也容易被产生误解。如果 Check() (支票) 类是从 Document (文档) 类继承而来的,那么 check.Print() 就很显然表示打印一张支票,而 check.PrintDocument() 看上去像要打印支票簿或是信用卡的对账单,而不像打印支票本本身。
1.7 准确使用对仗词
命名时遵守对仗词的命名规则有助于保持一致性,从而也提高可读性。像 first/last 这样的对仗词就很容易理解;而像 FileOPen() 和 _lclose() 这样的组合则不对称,容易使人迷惑。下列列出一些常见的对仗词组:
add/remove increment/decrement open/close
begin/end insert/delete show/hide
create/destroy lock/unclock source/target
first/last min/max start/stop
get/put next/previous up/down
get/set old/new
1.8 为常用操作确立命名规则
在某些系统里,区分不同类别的操作系统非常重要。而命名规则往往是指示这种区别最简单也是最可靠的方法。
在我们做过的一个项目的代码里,每个对象都被分配了一个唯一标识。我们忽视了为返回这种对象标识的子程序建立一个命名规则,以至于有了下面这些子程序名字:
employee.id.Get();
dependent.GetId();
supervisor();
candidate.id();
其中:Empoyee 类提供了其 id 对象,而该对象又进提供了 Get() 方法;Dependent 类提供了 GetId() 方法;Supervisor 类把 id 作为它的默认返回值;Candidate 类则认为 id 对象的默认返回值是 id,因此暴露了 id 对象。
到了项目中期,已经没有人能记住哪个对象应该用哪些子程序,但此时已经编写了太多的代码,已经无法返回再重新统一命名规则了。这样一来,项目组中每个人都不得不花费不必要的精力,去记住每个对象上采用的获取 id 的语法细节。而这些问题完全可以通过建立获取 id 的命名规则而避免。
2. 子程序的长度
理论上认为的子程序的最佳最大长度通常是一屏代码或打印出来一到两页的代码,也就是约 50 ~ 150 行代码。按这种精神,IMB 曾经把子程序的长度限制在 50 行之内。
多年来,人们已经在子程序的长度问题上积累了大量的研究成果,其中一些适用于现代编程,而另一些则不尽如此。
-
Basili 和 Perricone 所做的一项研究发现,子程序的长度与错误量成反比,即:随着子程序长度的增加(上至 200 行代码),每行代码所包含的错误数量就会减少。
-
另一项研究则发现,子程序的长度与错误量没有关联,而结构复杂度以及数据量却与错误量有关。
-
1986年所做的一项研究发现,短小的子程序(含有 32 行或者更少代码)与更低的成本或错误率无关。有证据表明,较长的子程序(含有 65 行或更多代码)使得每行代码的成本更低。
-
一项对 450 个子程序所做的研究发现,相对较长的子程序而言,短小的子程序(包括注释在内少于 143 行语句)中每行代码所含的错误数量要多 23%,而较长子程序的修改成本却高 2.4 倍。
-
另一项研究发现,平均长度为 100 到 150 行代码的子程序需要被修改的几率最低。
-
IBM 所做的一项研究发现,最容易出错的是那些超过 500 行代码的子程序。超过 500 行之后,子程序的出错率就会与其长度成正比。
那么,在面向对象的程序中,一大部分子程序都是访问器子程序,他们都非常短小。在任何时候,复杂的算法总会导致更长的子程序。这这种情况下,可以允许子程序的长度有序地增长到 100 至 200 行(不算源代码汇总的注释行和空行)。数十年的证据表明,这么长的子程序也和短小的子程序一样不易出错。
与其对子程序的长度强加限制,还不如让下面这些因素 —— 如子程序的内聚性、嵌套的层次、变量的数量、决策点(decision points)的数量、解释子程序用意所需要的注释数量以及其他一些跟复杂度相关的考虑事项等 —— 来决定子程序的长度。
这就是说,如果要编写一段超过 200 行代码的子程序,那你就要小心了。对于超过 200 行代码的子程序来说,没有哪项研究发现它能降低成本 和 / 或 降低出错率,而且在超过 200 行后,你迟早会在可读性方面遇到问题。