《代码整洁之道》读书笔记(二)之什么样的函数是好函数

什么是好的函数

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. 如何写出这样的函数

跟写文章一样,先想什么就写什么,然后不断打磨。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。

你可能感兴趣的:(《代码整洁之道》读书笔记(二)之什么样的函数是好函数)