代码整洁之函数书写准则!

短小

短小函数的第一规则

第二条规则还是要更短小

  • 每行都不应该有150个字符那么长,函数也不该有100行那么长

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

单一职责 只做一件事

只做一件事,如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事

判断函数是否不止做了一件事,还有就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现

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

设计模式中有单一职责原则,我们可以把这条原则理解为代码整洁之道中的函数单一职责原则

函数参数尽可能的少

每个函数一个且仅有抽象层级:

  • 我们想要让代码拥有自顶向下的阅读顺序
  • 让代码读起来像是一系列自顶向下的 起头段落是保持抽象层级协调一致的有效技巧
  • 把读代码想象成读报纸
  • 总分结构,或者总分总结构

switch语句:

  • 多态–将switch语句埋到抽象工厂底下,在系统的其他部分看不到,就还能容忍
    • 可以使用抽象工厂来进行改进

函数名称使用:描述性名称

  • testableHtml 改为SetupTeardownIncluder.render,好的名称价值怎么好评都不为过

  • 如果每个例程都让你感动深合已意,那就是简洁代码

函数参数:

  • 参数越少越好,参数越少越便于理解

一元函数的普遍形式:

  • 函数名称应能区分出来

问关于参数的问题,如 boolean fileExist("myFile");

将参数转换为其他什么东西,在输出之:

  • InputStream fileOpen("myFile");

事件:

  • 有输入参数而无输出参数,程序将函数看做一个事件,使用该参数修改系统状态

标识参数丑陋不堪,即如果标识为true将会这样做,标识为false将会这样做:

  • 向函数传入布尔值简直是骇人听闻的做法

如果出现了参数是Boolean 值,就要思考一下,函数是否只做了一件事情

  • 反复问自己 ,这个函数是不是做了一件事,或者承担了一项职责

二元函数:

  • 可以把某个参数转换成当前类的成员变量,从而无需再传递它
    • 当然有一些有两个参数更加合适Point(x,y) 这种就比较合理

三元函数:

  • 排序,琢磨,忽略的问题都会加倍体现,写三元函数之前一定要想清楚

参数列表:

  • 一个数量可变的参数等同于一个参数

函数命名应与参数形成动词/名词对,如writeField(name)

无副作用:

  • 函数承诺只做一件事,但还是会做其他被藏起来的事,例如时序性耦合

输出参数:

  • 面向对象语言中对输出参数的大部分需求已经消失了
  • 应避免使用输出参数,如果函数必须要修改某种状态,就修改所属对象的状态吧

看下这个例子:

1
appendFooter(s)

看到这个 首先就会 想把 s 添加什么东西,或者它把什么东西加到s 后面?

  • s 是输入 还是输出参数呢?
1
public void appendFooter(StringBuffer report);

看了函数的签名,我们大概明白了。

所以 在面向对象语言中,最好的做法

1
report.appendFooter() // 这样来调用。

分隔指令与询问

函数要么做什么事(do),要么回答什么事(boolean),但二者不可兼得

1
public boolean set(String attributer, String value);

用户调用:

1
2
if (set("username","frank")) {//...
}

看到这个代码,首先想 是否能够成功设置username 为frank, 还是要问,username 是否已经之前被设置过为frank了呢?

对看代码的人会比较困惑

所以 解决方案 就是 把做什么和是否成功分开

  • 防止发生混淆
1
2
3
if (exists("username")){
setAttribute("username","frank")
}

使用异常替代返回错误码

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

抽离try/catch代码块:

  • try/catch代码块丑陋不堪

  • 最好把try和catch代码块的主体部分抽离出来,另外形成函数

错误处理就是一件事:

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

Error.java依赖磁铁:

  • 返回错误码通常暗示某处有个类或是枚举定义了所有错误码

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

使用异常的好处,可以避免嵌套太深的层级

  • 代码可以写起来更加简化

来看一个例子:

  • 这里使用了很多的 嵌套的 if语句 这种很难让人理解,嵌套层级特别多,看起来很复杂
  • 对于这种代码 最好直接抛出异常就可以,直接 使用 try 语句 然后捕获具体的异常就可以了

而不是一层,一层的进行判断,嵌套起来,之后就很难维护这样的代码

对于try 代码块 丑陋不堪, 它们会搞乱代码结构,把错误处理和正常流程混为一谈

最好把 try 和 except 的主体部分抽离出来 单独形成函数

  • 这样以后维护也会方便一点,看起来代码清晰,代码结构简单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void delete(Page page){
try{

deletePageAndAllReferences(page);
}catch(Exception e){
logError(e);
}

}


private void deletePageAndAllReferences(Page page) throws Exception {
deltePage(page);
registry.deleteReference(page.name);
configKeys.deleteKeys(page.name.makeKey());
}


public void logError(Exception e ){
logger.log(e.getMessage)
}

别重复自己

别重复自己:重复可能是软件中一邪恶的根源

  • 其实可以这样说,重复可能是软件中一切邪恶的根源,许多原则与实践规则都是为控制与消除重复而创建的

仔细想一想,面向对象编程是如何将代码集中到基类,从而避免了冗余的

而面向方面编程(Aspect Oriented Programming)

面向组件编程(Component Oriented Programming)多少也是消除重复的一种策略

  • 这样看来,自子程序发明以来,软件开发领域的所有创新都是在不断尝试从源代码中消灭重复

重复而啰嗦的代码,乃万恶之源,我们要尽力避免

结构化编程:

  • 每个函数、函数中的每个代码块都应该有一个入口,一个出口,遵循这些规则
    • 意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句
  • 对于小函数,这些规则助益不大,只有在大函数中,这些规则才会有明显的好处

如何写出这样的函数

先想什么就写什么,然后再打磨它

  • 初稿也许粗陋无序,你就斟酌推敲,直到达到你心中的样子

最好的情况要配上一套单元测试,覆盖每一行丑陋的代码

然后开始打磨这些代码,分解函数,修改名称,消除重复。最后要保持测试可以顺利通过

我并不是一开始就按照规则写函数,我想没有人做得到