短小 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
不要指望一开始就按规则写函数
先写出来,用尽量完备的测试覆盖每行代码
打磨代码,分解函数,修改名称,消除重复
编程艺术是且一直是语言设计的艺术
大师级程序员把系统当作故事来讲,而不是当作程序来写