编写函数的建议
1、短小、缩进嵌套少
函数体越短小越好。
函数的缩进层级最好控制在两级以内
为了达到上面一点,if、else、while 等语句中的代码块最好就只有一行,且大抵是个函数调用语句,具有意义的函数名在代码块内更具说明性
2、函数应该只做一件事情
如何判断你的函数体中的代码只做了一件事情?
前言:在说明这件事情之前先举个例子来说明函数抽象层级(Level Of Abstraction)
- To do A, First do B, Then do C, Finally do D
- To do B, First do E, Then do F
- To do E, First do G
不难发挥一下想象力,把上面的过程抽象成在自顶向下广度搜索一颗树,树的深度(即树的层级被称之为一个抽象层级)。将上面 A、B、C…… 看成你的函数名,根据上述抽象层级的描述,不难发现,A单独在一个抽象层级,[B、C、D] 三者在同一个抽象层级。在《Clean Code》中有原话如下
编写函数毕竟是为了把大一些的概念(换而言之,函数的名称)拆分为另一抽象层上的一系列步骤
用上面的例子来解释这句话就可以是,编写函数 A 就是为了把 A 函数名所描述的大概念,变成同属于一个抽象层级中的 B、C、D 三个步骤,然后以此类推
- 注意:说成数据结构中的树只是为了方便理解,但是层级属于父子节点的相对概念,对于非隶属于同一父节点但又在树结构中属于同一深度的节点们来说不一定有关联关系,即假设 C 有子层级 [ C1、C2 ],那么你不能说 B 的下一子层级 [ E、F ] 和 [C1、C2] 处于同一抽象层级!只关心自顶向下,而不关心左右)
举个例子
public void CreateTestPage() // A{ IncludeSetups(); // B IncludeTestPageContent(); // C IncludeTeardowns(); // D}public void IncludeSetups() // B{ if(this.IsSuite()) { IncludeSuiteSetup(); // E } IncludeRegularSetup(); // F}public void IncludeSuiteSetup() // E{ var parentPage = FindParentSuitePage(); // G // add include statement with the path of the parentPage}
如何判断?
如果你已经理解了上面的铺垫,那么这句话的答案也只有一句话,即书中原文
如果函数只是做了该函数名下同一抽象层上的步骤,则函数就只做了一件事
无法再被划分成为很多区段,只做一件事情的函数无法被合理地划分成为多个区段,划分区段的意思就是说函数体中的若干行代码组成的代码块完全可以被整合起来然后写到另外一个函数中去作为其函数名所描述的功能性代码,而在原函数体中,那段代码块就变成了一个函数调用了。
有了判断基础,那编写一个函数的步骤?(不属于书本范畴,是笔者自己总结后得出)
- 整理好你的需求
- 拆分成自顶向下的抽象层级,体现在代码上就是若干函数自顶向下的调用
- 检查在同一函数中的代码块/调用函数是否属于是同一抽象层级的若干步骤,如果不是就再拆
最后,不建议一开始就把所有事情写在一个函数体中,用代码块的形式拆分开,最后再改造成若干函数调用的形式。
3、switch 语句、if...else if....else 语句
问题:这种语句天生就是用来干 N 件事情,这 N 件事情是否在同一抽象层级决定了函数是否只干一件事情,要想达到不同条件干的事情属于同一层级(即让函数只干一件事)在《Clean Code》书本中作者说了,很难。我直觉上赞同这一说法。
解决方法:原书中作者的解决方案略微有点妥协的意思,原文如下
不幸的是我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复
意思就是抽象层级越高,影响到的代码及关联函数越多(自顶向下),这种比较糟糕的代码如果无法避免或者避免的成本过高那么就尽量埋藏在低抽象层级中
例子:《Clean Code》 P36 工厂模式
4、参数
结论:
- 参数个数使用优先级:0 > 1 >> 2 >>> 3 并且无论如何没有足够特殊的理由,请别用 3 个以上的参数。
- 尽量不用的参数类型:输出参数、标识参数
- 对于不同参数个数的函数,尽量遵循符合直觉的普遍形式
对以上结论的补充都在下文
一元函数(单参数函数)的普遍形式
总共三种形式,建议不要写超脱于这三种形式之外的单参数函数
- 问询,如 bool fileExists("FileName")
- 转换,如 InputStream fileOpen("FileName")
- 事件,如 void EventFunction(Enum caseCode)
这三种形式比较符合直觉,尽量不要使用输出参数在一元函数中,如果函数要对输入的参数进行转化,那么结果就应当体现在返回值上,例如 StringBuffer transform(StringBuffer in) 就是要比 void transform(StringBuffer out) 强
标识参数!别用!
标识参数就是向函数传入一个信号来标识不同的case,千万千万不要这样做,即类似 void Func(bool flag),相当于大声宣布函数不止做了一件事情,而且重构起来会相当麻烦。
输出参数!谨慎使用!
二元函数、三元函数
结论:在可接受的代价之内,能用一元函数就别用二元函数了,三元同理。
二元函数相对一元函数增加了一些些理解成本。二元转一元有很多种方式,具体场景具体操作。当然有些东西天然就是两个参数的,例如 Point p = new Point(0, 0) 。这事因为在笛卡尔坐标系中单个值就是由有序的两个变量组成,而像 assetEquals(excepted, actual) 这样的二元函数,两个参数没有像笛卡尔坐标点一样特性(单个值的有序组成部分)在顺序上,两个参数颠倒也没什么问题。
参数对象
结论:如果函数需要两个、三个或者三个以上的参数,那就说明其中一些参数需要封装成为类
例子:
Circle makeCircle(double x, double y, double radius)
Circle makeCircle(Point cener, double radius)
无副作用(非常重要!!!)
函数声明在前面的原则中应当只干一件事情,但是如果它还做了意料之外的事情,做出未能预期的改动,将会是很糟糕的事情。笔者曾经就干过这样的事情,在一个声明为 get_xxx() 的取值函数中,做了对另外一些变量的修改。导致后期有些 bug 莫名其妙。还有包括干过一个检验函数,is_xxx_can_level_up() 中,在里面判断失败时候会发送一个错误 tip,但是从名字上来看,发送错误 tip 不应该在此函数内实现。
分隔指令与询问
就像上面说的,我干过的蠢事,在一个检验函数,is_xxx_can_level_up() 中,在里面判断失败时候会发送一个错误 tip。其实应该把发送 tip 的指令和问询分开来。做到 函数要么做什么事情,要么回答什么事情,尽量避免混在一起!
使用异常替代返回错误代码
- 如果使用错误代码的方式,则很容易引起多层 if 嵌套,当 if 条件不满足的时候来处理异常情况。这是因为采用这种方式导致使用者需要立即处理错误,而不像异常一样可以“往上抛“或者”集中处理“
- 返回错误码通常暗示某处有个类或者枚举定义了所有的错误码,这样的类就是一块“依赖磁铁”,许多类都要导入和使用它(其实在抄这段话的时候,笔者项目就已经有这样的情况了!)当 Error 类或者枚举被修改时,其他所有类都要重新编译和部署,导致了修改程序的人需要去维护这些麻烦事情,变得不愿意新增比较任何 Error Code,导致后面就会频繁使用类似 Other Error 这种错误码。使用异常来代替错误码,新异常可以从旧异常中派生出来!
抽离 Try / Catch 代码块
其实相信你也写过很多在函数体中穿插很多处理错误的 try / catch 块。在《clean code》中,作者认为这把处理错误和正常流程混为一谈,其实我非常赞同这一点,实际上这本质就是两件事情,穿插在一起不方便理解和拓展修改。建议是,函数只应该做一件事情原则下,处理错误就应当被当做一件事情,也就意味着如果关键字 try 在某个函数中存在,那它就是这个函数的第一个单词,而且在 catch / finally 后面也不该有其他内容
消除重复
书中说道重复可能是一切邪恶的根源。许多原则和世间都是为了控制与消除重复而创建(例如数据库范式,AOP,面向组件编程),修改重复的东西需要改很多遍,代码变得臃肿。如果你有机会去重构,面对每一次重构,都有一个基本任务,那就是——消除重复!
结构化编程
Dijkstra 结构编程规则,提倡每个函数的每个代码块都一个有一个入口和一个出口,意味着函数只有一个 return 语句,循环中不能有 break 和 continue (笔者说实话,这个循环的原则,真的很难遵守啊),而且永永远远不能有任何 goto 语句。其实鲍勃大叔认为这个在小函数中收益不大,大函数中就有明显好处,那么我的理解只能到遵守在大函数中只有一个 return 语句(而且在实践中这个的确对于大函数的理解比较有作用)
5、最后
其实这篇摘抄总结的文章我可能写了有一个月了,陆陆续续到现在才读完这一章(平时太忙了),在这一个月的试炼中,其实采用了其中的原则去编程的确收益很大,例如函数需要短小 分离抽象层次只做一件事情,我遵守这一规则之后发现重构变得轻松非常多。包括后面的参数等,我还没有过多实践来切实体会到其中的好处(虽然想当然的感觉有道理,但是只有亲身体体验过才知道这些原则之助益),实际上出了原则本身带来的好处,我对其中若干原则实施的过程中还体会到了,其实多条原则本身不是平行的,可能产生矛盾,而且都有遵守成本(最大的体会就是设计出遵守原则的代码需要时间成本),在实践中力求尽量遵守对当前编程需求下助益最大的,有所取舍。