0%

代码整洁之道 - 函数

短小 Small

函数的第一条规则是要 短小,第二条规则是要 更短小

  • 20 行封顶最佳

例子:

1
2
3
4
5
6
7
public static String renderPageWithSetupsAndTearDowns(PageData pageData, boolean isSuite) throws Exception
{
if (isTestPage(pageData)) {
includeSetupAndTeardownPages(pageData, isSuite);
}
return pageData.getHtml();
}

代码块与缩进 Blocks and Indenting

if 、else 和 while 语句等,其中的代码块应该只占一行(should be one line long),通常该行是一个函数调用

  • 函数的缩进层级不该多于一层或两层

  • 这样的函数易于阅读和理解

只做一件事 Do One Thing

  • 函数应该做一件事。做好这件事,只做这一件事。

  • 函数的目的 => 把较大的概念 拆分为 另一抽象层上的 一系列步骤

  • 如果步骤在同一抽象层上,我们就可以认为是同一件事

  • 判断函数可否做了一件事 -> 看其是否可再被拆分出一个函数

  • 只做一件事的函数,无法被合理切分为多个区段

每个函数一个抽象层级 One Level of Abstraction per Function

  • 要确保函数只做一件事,函数中语句必须在同一抽象层级

  • The Stepdown Rule 向下规则:让代码读起来像是一系列 自顶向下的 TO 开头的段落是保持抽象层级协调一致的有效技巧

Switch 语句 Switch Statements

switch 不可能只做一件事,但我们可以把它埋藏在 较低 的抽象层级

解决方案:将 switch 埋藏到 抽象工厂 底下

1
2
3
4
5
6
7
8
9
10
11
12
13
public Money calculatePay(Employee e)
{
switch (e.type) {
    case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidOperation();
    }
}

该函数有几个问题:

  1. 太长,出现新的雇员类型时会更长

  2. 明显做了不止一件事

  3. 违反了单一职责原则,因为有好几个可以修改它的理由

  4. 违反了开闭原则,每当添加新类型时,必须修改该函数

更好的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Employee {
public abstract boolean isPayDay();
    public abstract Money calculatePay();
    public abstract void deliverPay(Money pay);
}
-------------------
public interface IEmployeeFactory {
public Employee makeEmployee(EmployeeRecord r);
}
-------------------
public EmployeeFactory : IEmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) {
    switch (r.type) {
        case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmployee(r);
            default:
                throw new InvalidOperation();            
        }
    }
}

使用具有描述性的名称 Use Descriptive Names

  • 函数越短小,功能越集中,越便于起个好名字

  • 长而具有描述性的名称 比 短而令人费解的名称 好

  • 长而具有描述性的名称 比 描述性的长注释 好

  • 描述性的名称 能理清 你关于模块 的设计思路

  • 命名方式要保持一致

函数参数 Function Arguments

  • 最理想的参数个数是 0,其次是 1,再次是 2

  • 尽量避免3,有足够的理由才能用3个以上的参数

  • 参数与函数处于不同的抽象层级,要求你了解目前并不重要的细节

  • 尽量不要用输出参数,一般情况下难以理解

单参数函数的普遍形式 Common Monadic Forms

  • 问关于参数的问题 bool fileExists("MyFile")

  • 操作参数,转换为其他的东西 InputStream fileOpen("MyFile")

  • 事件 Event,程序将函数看成一个事件,使用该参数修改系统状态

  • 尽量避免编写不遵循这些形式的单参数函数

标识参数 Flag Arguments

标识参数是丑陋的,一旦用了标识参数,意味着一定违反了SRP,此时应该拆分函数

双参数函数 Dyadic Functions

尽量利用一些机制将其转换成单参数函数

三参数函数 Triads

在写三参数函数前一定要想清楚

参数对象 Argument Objects

如果函数看起来需要超过2个以上参数,此时就意味着其中一些参数应该被封装成类了

1
2
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

参数列表 Argument Lists

动词与关键字 Verbs and Keywords

对于单参数函数,函数和参数应当形成一种非常良好的动词/名词对形式,例如 write(name). 更好的名称也可以是 writeField(name) ,这告诉我们 name 是一个 field

无副作用 Have No Side Effects

副作用是一种谎言,函数承诺做一件事,却悄悄地做了另一件事。这会造成意料之外的影响如:

  1. 古怪的时序性耦合

  2. 顺序依赖

函数名一定要能体现其要做的事

输出参数 Output Arguments

  • 如果你被迫检查函数签名,就得花时间做一些重构,应该避免这种中断思路的事

  • 应避免使用输出参数,因为 this 也有输出的意味

  • 如果函数必须要修改某种状态,就修改所属对象的状态

分隔指令与询问 Command Query Separation

  • 函数要么做什么事,要么回答什么事,二者不可兼得

使用异常替代返回错误码 Prefer Exception to Returning Error Codes

  • 从指令式函数返回错误码略微违反了指令与询问分隔的原则

  • 如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,如:

1
2
3
4
5
6
7
try {
deletePage(page);
registry.deleteReference(page.name);
configKeyd.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}

抽离 try/catch 代码块 Extract Try/Catch Blocks

try/catch 代码块,把错误处理与正常流程混为一谈, 最好把 try 和 catch 代码块的主体部分抽离出来,另外形成函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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());
}

该例中:

  • delete 函数只与错误处理有关

  • deletePageAndAllReferences 只与完全删除一个page有关,不用管错误处理

错误处理就是一件事 Error Handling Is One Thing

函数应该只做一件事,错误处理就是一件事,这意味着:

如果在某个函数中存在 try,它就应该是这个函数的第一个单词,且在 catch/finally 代码块后面也不该有其他内容。

Error.java 依赖磁铁 The Error.java Dependency Magnet

返回错误通常定义成一个类或枚举,很多其他类都会导入和使用它,意味着:

当修改 Error.java 时,所有其他类都需要重新编译和部署 - 这是不好的一面

别重复自己 Do not Repeat Yourself

重复可能是软件中一些邪恶的根源,许多原则和实践规则都是为了控制与消除重复而创建的

例如:

  • Codd 数据库范式就是为了消除数据重复而服务的

  • 面向对象编程把代码集中到基类,从而避免冗余

  • 面向切面编程、面向组件编程,多少也是为了消除重复的一种策略

结构化编程 Structured Programming

这是 Edsger Dijkstra 提出的编程规则:每个函数、函数中的每个代码块都应该有一个入口,一个出口,遵循该原则,意味着:

  • 每个函数只该有一个 return 语句

  • 循环中不能有 breakcontinue 语句

  • 永远不能有任何 goto 语句

但是,对于小函数,这些规则助益不大,所以尽量保持函数短小,偶尔出现的 return breakcontinue 语句没有坏处

小结 Conclusion

  • 不要指望一开始就按规则写函数

  • 先写出来,用尽量完备的测试覆盖每行代码

  • 打磨代码,分解函数,修改名称,消除重复

  • 编程艺术是且一直是语言设计的艺术

  • 大师级程序员把系统当作故事来讲,而不是当作程序来写