代码大全笔记之高质量的子程序
高质量的子程序 Hign-Quality Routines
什么是子程序?
子程序是为实现一个特定目的而编写的一个可被调用的方法或过程。Java中为方法(method)。
1 创建子程序的正当理由
1.1 降低复杂度
可以通过创建子程序来隐藏一些信息,可以直接调用该子程序而无需了解其内部工作细节。
1.2 引入中间、易懂的抽象
把一段代码放入一个名字恰当的子程序内,时说明这段代码用意最好的方法之一。
修改前:
if (node != null) {
while (node.next != null) {
node = node.next;
leafName = node.name;
}
} else {
leafName = "";
}
修改后:
leafName = GetLeafNode(node);
取一个好名字就足够证明它的用意了。
1.3 避免代码重复
如果在两段子程序内编写相似的代码,应该把两段子程序中的重复代码提取出来,有两种方法:
- 将其中相同的部分放入一个基类,然后把两段程序中的差异代码放入派生类中。
- 把相同的代码放入新的子程序中,再让其余的代码来调用这个子程序。
与代码重复出现相比,让相同的代码只出现一次可以节约空间。代码改动起来也更方便,只需要在一处修改即可。
1.4 支持子类化
1.5 隐藏顺序
把处理事件的顺序隐藏起来是个好主意。
案例:
- 一个程序通常都是先从用户那里读取数据,然后再从文件中读取辅助数据,那么,无论是从用户那里读取数据的子程序(方法)还是从文件中读取数据的子程序(方法),都不应该依赖另一个程序是否已执行。
- 假设写了两行代码,先读取栈顶的数值,然后减少
stackTop
变量的值。你应该把这两行代码放到一个叫popStack()
的子程序(方法)中。从而将这两行代码所必须执行的顺序隐藏起来。
1.6 提高可移植性
1.7 简化复杂的布尔判断
为了理解程序的流程,通常没有必要去研究那些复杂的布尔判断细节。应该把这些判断放入到函数中,以提高代码的可读性:
- 可以把判断的细节放到一边;
- 一个具有描述性的函数名字可以概括出该判断的目的。
1.8 似乎过于简单而没有必要写成子程序的操作(额外说明)
编写有效的子程序时,一个最大的心理障碍是不情愿为了一个简单目的而编写一个简单子程序。
1.8.1 提高可读性
小的子程序有很多优点,其一便是它们能够提高可读性。
案例:
// 某种计算
points = deviceUnits * (POINT_PER_INCH / DeviceUnitsPerInch);
大多数人可以看懂,但是可以表示的更加清楚
public int deviceUnitsToPoints(int Units) {
// 详情省略
}
调用:
int deviceUnits;
points = deviceUnitsToPoints(deviceUnits);
这行代码更具有可读性。
1.8.2 简单的操作常常会变成复杂操作
另一个原因就是简单的操作常常会变成复杂操作。
维护代码的时候扩展了函数,添加了几行代码,如果是原来的那样,就需要增加几十行代码,使用一个简单的子程序,就把代码从几十行减到了 3 行。
2 在子程序层上设计
目标是让每一个子程序只把一件事情做好,不再做其他任何事情。
3 好的子程序名字
3.1 描述子程序所做的事情
3.2 避免使用无意义、模糊或表述不清的动词
3.3 不要仅通过数字来形成不同的子程序名字
3.4 根据需要确定子程序名字的长度
变量名最佳长度是 9 到 15 个字符。
3.5 给函数命名时要对返回值有所描述
函数有返回值,因此,函数的命名要应该针对其返回值进行。
好的函数名案例
- cos()
- customId.next()
- printer.isReady()
- pen.currentColor
它们精确的表达了该函数将要返回的结果。
3.6 给过程起名时使用强烈的动词加宾语的形式
在面向对象的语言中,你不用在过程中加入对象的名字(宾语),因为对象本身已经包含在调用语句中了。
你会用
- document.print()
- orderInfo.check()
- monthlyRevenues.calc()
等语句调用子程序。而诸如 document.printDocument()
这样的语句则显得臃肿,并且当它们在派生类中被调用时也容易产生误解。如果 Check
(支票)类是从 Document
(文档)类继承而来的,那么 check.print()
就很显然表示打印一张支票,而 check.printDocument()
看上去像是要打印支票簿或是信用卡的对账单,而不像打印支票本身。
3.7 准确使用对仗词
命名时遵守对仗词的命名规则有助于保持一致性。从而也提高可读性。
常见的对仗词组:
对仗词 | 含义 |
---|---|
add/remove | 添加/移除 |
begin/end | |
create/destory | |
first/last | |
get/put | |
get/set | |
increment/decrement | |
insert/delete | |
lock/unlock | |
min/max | |
next/previous | |
old/new | |
open/close | |
show/hide | |
source/target | |
start/stop | |
up/down |
3.8 为常用操作确定命名规则
4 子程序可以有多长
可以允许子程序的长度有序的增加到 100 行至 200 行。但是,与其对子程序的长度加以限制,还不如让下面的因素—如子程序的内聚性、嵌套的层次、变量的数量、决策点的数量、解释子程序用意所需的注释数量以及其他一些复杂度相关的考虑事项来决定子程序的长度。
5 如何使用子程序参数
子程序之间的接口是程序中最容易出错的部分之一。
有研究发现,程序之间相互通信时所发生的错误可以占 39%。
5.1 按照输入-修改-输出的顺序排列参数
不要随机地按照字母的顺序排列参数,而应该先列出仅作为输入用途的参数,然后是既作为输入又作为输出用途的参数,最后才是仅作为输出用途的参数。
案例:Servlet 的 request 和 response
5.2 考虑自己创建 in 和 out 关键字
5.3 如果几个子程序都用了类似的一些参数,应该让这些参数的排列顺序保持一致
5.4 使用所有的参数
既然往子程序中传递一个参数,就一定要用到这个参数。如果你不用它,就把它从子程序的接口中删去。
5.5 不要把子程序的参数用作工作变量
案例:
// 不恰当地使用参数
public int sample(int inputVal) {
inputVal = inputVal + 1;
inputVal = inputVal * 5;
return inputVal;
}
更好的方法是明确地引入一些工作变量,从而避免造成当前或日后的麻烦。
// 正确的使用输入参数(伪代码)
public int sample(int inputVal) {
int workingVal = inputVal;
workingVal = workingVal + 1;
workingVal = workingVal * 5;
return workingVal;
}
5.6 在接口中对参数的假定加以说明
如果你假定了传递给子程序的参数具有某种特征,那就要对这种假定加以说明。
一种比用注释还好的方法,是在代码中使用断言。
应该对哪些接口的假定进行说明呢?
- 参数是仅用于输入的、要被修改的、还是仅用于输出的;
- 表示数量的参数的单位;
- 如果没有枚举类型的话,应说明状态代码和错误码的含义;
- 所能接受的数值的范围;
- 不该出现的特定数值。
5.7 把子程序的参数个数限制在大约 7 个以内
5.8 考虑对参数采用某种表示输入、修改、输出的命名规则
5.9 为子程序传递用以维持其接口抽象的变量或对象
接口参数传递变量还是对象问题的要害:子程序要表达何种抽象?
如果要表达的抽象的子程序期望 3 项特定数据,但这 3 项数据只是碰巧由同一个对象所提供的,那就应该单独传递这 3 项数据;如果子程序接口想要表达的是一直想要拥有某给特定的对象,且该子程序要对这一对象执行这样那样的操作,如果单独传递 3 项特定的数据,那就是破坏了接口的抽象。
- 如果你采用了传递整个对象的做法,并发现自己是先创建的对象,把被调用子程序所需的 3 项数据填入该对象,在调用子程序后又从对象中取出 3 项数据的值,那就是说明你应该值传递那 3 项数据而不是整个对象。
- 如果你发现自己经常需要修改子程序的参数表,而每次修改的参数都是来自于同一个对象,那就说明你应该传递整个对象而不是个别数据项了。