什么是好的函数
1. 短小
函数的第一规则是要短小。第二条规则是还要更短小。
函数二十行最佳。
函数的缩进层级不应该超过二层。(这个有点难)
2. 只做一件事
函数应该做一件事。做好这件事。只做这件事。
3. 每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一个抽象层级上。
函数中混杂不同抽象层级,让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
4. switch语句不可避免,使用多态隐藏到较低的抽象层
switch语句天生就是要做N件事,不幸地是我们总无法避开,不过还是能够确保每个switch语句都埋藏在较低的抽象层,而且永远不重复。
5. 使用描述性的名称
花时间给函数取个好的具有描述性的名字是很值得的,不要害怕长名字。函数功能越集中、越短小越容易取名字,如果你发现你对你的函数很难取名字的时候,是该考虑这个函数是否需要进行拆分了。
6. 函数参数
最理想的参数数量是零,其次是一,再次是二,应尽量避免三。有足够特殊的理由才能用三个以上参数——所以无论如何也不要这么做。
函数尽量不要有标识参数。向函数传入boolean值简直就是骇人听闻的做法。
如果函数看起来需要两个、三个或三个以上的参数,就说明其中一些参数应该封装为类了。例如,下面两个声明的差别:
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。
例如:
write(name); // 不管name是什么,都要被write
writeField(name); // 它告诉读者name是一个Field
7. 无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。
输出参数
应该避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。
用作输出而非输入的参数,让人迷惑,例如:
appendFooter(s);
这个函数是把s
添加到什么后面吗?或者它把什么东西添加到了s
后面? s
是输入参数还是输出参数? 稍微花点时间看一下函数签名:
public void appendFooter(StringBuilder report);
事情清楚了,但付出了检查函数签名的代价。应该避免这种中断思路的事。
在面向对象语言中对输出参数的大部分需求已经消失了,因为this也有输出参数的意味在内。换言之,最好是这样调用appendFooter
:
report.appendFooter();
8. 使用异常代替返回错误码
返回错误码可能导致更深层次的嵌套结构。当返回错误码时就是在要求调用者立即处理错误。如果使用异常代替返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。
// 嵌套结构层次深
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page deleted");
} else {
logger.log(configKey not deleted);
}
} else {
logger.log("deleteReference from registry failed");
}
} else {
logger.log("deleted failed");
return E_ERROR;
}
// 使用异常处理代替返回错误码
try {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}
抽离try/catch代码块
try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。意味着如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finaly代码块后面也不该有其他内容。(PS:好严格)
Error.java 依赖磁铁
返回错误码通常意味着某处有个类或是枚举,定义了所有错误码。
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT,
}
这样的类就是一块依赖磁铁,其他许多类都得导入和使用它。当Error枚举修改时,所有这些其他类都需要重新编译和部署。
使用异常替代错误码,新异常就可以从异常类派生出来,无需重新编译或重新部署。
9. 别重复自己(DRY原则)
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为了控制与消除重复而创建的。
10. 结构化编程
Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
这个规范对于小函数帮助不大,只有在大函数中才有明显的好处。
11. 如何写出这样的函数
跟写文章一样,先想什么就写什么,然后不断打磨。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。