短小 Small
函数的第一条规则是要 短小,第二条规则是要 更短小
- 20 行封顶最佳
例子:
1 | public static String renderPageWithSetupsAndTearDowns(PageData pageData, boolean isSuite) throws Exception |
代码块与缩进 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 | public Money calculatePay(Employee e) |
该函数有几个问题:
太长,出现新的雇员类型时会更长
明显做了不止一件事
违反了单一职责原则,因为有好几个可以修改它的理由
违反了开闭原则,每当添加新类型时,必须修改该函数
更好的写法:
1 | public abstract class Employee { |
使用具有描述性的名称 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 | Circle makeCircle(double x, double y, double radius); |
参数列表 Argument Lists
略
动词与关键字 Verbs and Keywords
对于单参数函数,函数和参数应当形成一种非常良好的动词/名词对形式,例如 write(name). 更好的名称也可以是 writeField(name) ,这告诉我们 name 是一个 field
无副作用 Have No Side Effects
副作用是一种谎言,函数承诺做一件事,却悄悄地做了另一件事。这会造成意料之外的影响如:
古怪的时序性耦合
顺序依赖
函数名一定要能体现其要做的事
输出参数 Output Arguments
如果你被迫检查函数签名,就得花时间做一些重构,应该避免这种中断思路的事
应避免使用输出参数,因为 this 也有输出的意味
如果函数必须要修改某种状态,就修改所属对象的状态
分隔指令与询问 Command Query Separation
- 函数要么做什么事,要么回答什么事,二者不可兼得
使用异常替代返回错误码 Prefer Exception to Returning Error Codes
从指令式函数返回错误码略微违反了指令与询问分隔的原则
如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,如:
1 | try { |
抽离 try/catch 代码块 Extract Try/Catch Blocks
try/catch 代码块,把错误处理与正常流程混为一谈, 最好把 try 和 catch 代码块的主体部分抽离出来,另外形成函数。
1 | public void delete(Page page) { |
该例中:
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语句循环中不能有
break或continue语句永远不能有任何
goto语句
但是,对于小函数,这些规则助益不大,所以尽量保持函数短小,偶尔出现的 return break 和 continue 语句没有坏处
小结 Conclusion
不要指望一开始就按规则写函数
先写出来,用尽量完备的测试覆盖每行代码
打磨代码,分解函数,修改名称,消除重复
编程艺术是且一直是语言设计的艺术
大师级程序员把系统当作故事来讲,而不是当作程序来写