代码整洁之道_03函数

第三章 函数

1. 短小

  • 函数的第一条规则是短小,20行封顶最佳;
  • if, else, while 语句中的代码块应该只占一行(函数调用语句);
  • 函数不应该大到足以容纳嵌套结构,缩进层级不该多于两层。

2. 只做一件事

  • 函数应该只做一件事,并做好这件事;
  • 判断函数是否不止做了一件事:能否再拆出一个函数。

3. 每个函数一个抽象层级

  • 函数中混杂不同抽象层级,往往会让人迷惑,无法判断某个表达式是基础概念还是细节;
  • 破窗理论:一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来;
  • 让代码拥有自顶向下的阅读顺序,每个函数后面都跟着位于下一抽象层级的函数,每一层保持相同抽象层级;

4. switch语句

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

问题:

  1. 太长,并且会增长(出现新类型时);

  2. 违反了单一权责原则

  3. 违反了开放闭合原则,添加新类型时,必须修改函数;

  4. 连锁反应导致出现大量类似结构的函数:

    1
    2
    3
    isPayday(Employee e, Date date);

    deliverPay(Employee e, Money pay)

解决方案:

将switch语句埋藏到抽象工厂底下。

该工厂使用 switch 语句为 Employee 的派生物创建适当的实体,而不同的函数,如calculatePay, isPayday 和 deliverPay 等,则借由Employee接口多态地接受派遣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Employee 与 工厂
public abstract class Employee{
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}

------------------------------------
public interface EmployeeFactory{
public Employee makeEmployee(EmployeeRecord r) throw InvalidEmployeeType;
}
------------------------------------

public class EmployeeFactoryImpl implements EmployeeFactory{
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType{
switch(r.type){
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(e.type);
}
}
}

5. 使用具有描述性的名称

  • 别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好;
  • 别害怕花时间起名字
  • 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如:
    • includeSetupAndTeardownPages
    • includeSetupPages
    • includeSuiteSetupPage
    • includeSetupPage等。

6. 函数参数

  • 0个最理想,有足够特殊的理由才能用3个以上参数

  • 参数不易对付,带有太多概念性。参数与函数名处在不同的抽象层级,他要求你了解目前并不特别重要的细节。

    includeSetupPage() includeSetupPageInto(newPageContent) 易于理解。

  • 多参数让测试为难:各种组合的测试用例;

  • 输出参数比输入参数还要难理解;

6.1 单参数函数的普遍形式

两种普遍形式:

  1. asking question:

    1
    boolean fileExists("myFile")
  2. operating:

    1
    InputStream fileOpen("MyFile")

不普遍但有用:

  • 事件,有输入参数而无输出参数,使用该参数修改系统状态。

    尽量避免写出不遵守这些形式的单参数函数,如使用输出参数而非返回值void includeSetupPageInto(StringBuffer pageText).

    如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。

6.2 标识参数——丑陋不堪

  • 标识参数丑陋不堪,方法签名变得复杂,并意味着函数不止做一件事儿。

6.3 双参数函数

尽量利用一些机制将其转换成单参数函数: writeField(outputStream, name)

  • writeField方法写成outputStream的成员之一:outputStream.writeField(name)
  • outputStream写成当前类的成员变量,从而无需再传递它;
  • 分离出类似于 FieldWriter的新类,在其构造器中采用outputStream

6.4 三参数函数

考虑清楚!!!

6.5 参数对象

如果函数看起来需要2个、3个或3个以上参数,就说明其中一些参数应该封装为类了。

或,当一组参数被共同传递(x, y),往往就该被封装为类。

6.6 参数列表

可变参数

6.7 动词与关键字

  • 对于单参数函数,函数和参数应当形成一种非常良好的动词/名词对形式。如write(name)

  • 函数名称的关键字

    • assertEquals(expected, actual) 改成:assertExpectedEqualActual(expected, actual),大大减轻记忆参数顺序的负担。

7. 无副作用

副作用是一种谎言。函数只做一件事,但偶尔会做其他被藏起来的事儿:

  • 对自己类中的变量做出未能预期的改动;
  • 把变量搞成向函数传递的参数或是系统全局变量。

避免使用输出参数

1
public void appendFooter(StringBuffer report)

转换为:

1
report.appendFooter();

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

8.分隔指令与询问

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

  • 修改某对象状态(指令);
  • 返回该对象的有关信息(询问);

9.使用异常替代返回错误码

从指令函数返回错误码略微违反了指令与询问分隔的原则,并引起更深层次的嵌套结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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("delete failed");
return E_ERROR;
}

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

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

9.1 抽离 try/catch 代码块

try/catch 代码块丑陋不堪。他们搞乱了代码结构,把错误处理与正常流程混为一谈。

最好把 trycatch代码块的主体部分抽离出来,另外形成函数:

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());
}

9.2 错误处理就是一件事儿

函数应该只做一件事儿——错误处理就是一件事儿。

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

9.3 Error.java 依赖磁铁

错误类/枚举就是一块依赖磁铁,其他许多类都得导入和使用它。当枚举修改时,其他所有类都需要重新编译和部署。

1
2
3
4
5
6
7
8
public enum Error {
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
解决:

使用异常替代错误码,新异常就可以从异常类派生出来,而无需重新编译和部署。

参见第七章:错误处理

10. 别重复自己

重复可能是软件中一切邪恶的根源。

后记1:如何写出这样的函数

写代码和写别的东西很像。在写论文或文章时,先是想到什么就写什么,然后再打磨它。初稿也许粗陋无序,可以对其斟酌推敲,直至达到你心中的样子。

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

总结:编程是门语言艺术!!!不能太懒!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// SetupTeardownIncluder 程序
package fitnesse.html;

import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;

public class SetupTeardownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;

public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}

public static String render(PageData pageData, boolean isSuite)
throws Exception {
return new SetupTeardownIncluder(pageData).render(isSuite);
}

private SetupTeardownIncluder(PageData pageData) {
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}

private String render(boolean isSuite) throws Exception {
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}

private boolean isTestPage() throws Exception {
return pageData.hasAttribute("Test");
}

private void includeSetupAndTeardownPages() throws Exception {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}

private void includeSetupPages() throws Exception {
if (isSuite)
includeSuiteSetupPage();
includeSetupPage();
}

private void includeSuiteSetupPage() throws Exception {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}

private void includeSetupPage() throws Exception {
include("SetUp", "-setup");
}

private void includePageContent() throws Exception {
newPageContent.append(pageData.getContent());
}

private void includeTeardownPages() throws Exception {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}

private void includeTeardownPage() throws Exception {
include("TearDown", "-teardown");
}

private void includeSuiteTeardownPage() throws Exception {
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}

private void updatePageContent() throws Exception {
pageData.setContent(newPageContent.toString());
}

private void include(String pageName, String arg) throws Exception {
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}

private WikiPage findInheritedPage(String pageName) throws Exception {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}

private String getPathNameForPage(WikiPage page) throws Exception {
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}

private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
}