代码整洁之道 - 函数
快速指南
以下是文中关于写好函数的几个关键点
- 短小
- 只做一件事
- 每个函数一个抽象层级
- 使用描述性的名称
- 函数参数,不要超过3个
- 无副作用
- 使用异常代替返回错误码
- 别重复自己
- 结构化编程
短小
函数的第一规则是要 短小。 第二条规则是还要更短小。
那么函数到底应该多长呢?
每个 函数都只说一 件事。 而且, 每个函数都依序把你带到下一个函数。 这就是函数应该达到的短小程度!
只做一件事
如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。
每个函数一个抽象层级
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。
函数中混杂不同抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来。
如何做到呢?
自顶向下读代码:向下规则
switch语句的优化
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);
}
}
上面这个函数存在几个问题:
- 函数太长(这也算长??)
- 明显做了不止一件事
- 违反单一职责原则,因为有好几个理由修改它
- 违反开闭原则(对扩展开放对修改关闭),因为每添加新的类型就需要修改它
解决办法:
通过抽象工厂解决。
使用描述性名称
好名称的价值怎么好评都不为过。记住沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码。”。
要遵循这一原则,为只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字。
函数参数
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。
单参数函数
使用单参数函数的两种普遍理由:
- 询问关于该参数的问题,如isFileExists("file")
- 操作该参数,将其转换为其他对象,再输出,如InputStream fileOpen("file")
另一种不太普遍但仍然有用的单参数形式,就是事件。
在这种形式中,有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,例如void passwordAttemptFailedNtimes(intattempts)。
标识参数
render(boolean isSuite),这种参数就是标识参数,这种函数就不止做一件事。应该把函数一分为二:renderForSuite()和renderForSingleTest()。
参数对象
当参数过多时,就说明其中一些参数应该封装为类了。
动词与关键词
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。例如,write(name)就相当令人认同。不管这个“name”是什么,都要被“write”。更好的名称大概是writeField(name),它告诉我们,“name”是一个“field”。
无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。
例如, 在checkPassword函数中同时做了Session创建的事情。这样,checkPassword方法就是有副作用的。
对于这个例子,可以重命名函数为checkPasswordAndInitializeSession,虽然那还是违反了“只做一件事”的规则。但至少方法是明确的,不会给调用方带来困惑和误解。
输出参数
普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。
分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。
例如下面的例子:
public boolean set( String attribute, String value);
真正的解决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists(" username")) {
setAttribute(" username", "unclebob");
...
}
使用异常代替返回错误码
从指令式函数返回错误码轻微违反了指令与询问分隔的规则。
另一方面,如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化。
另外,如果使用错误码,那么就会存在Error的枚举类,这个类就是一块依赖磁铁。其他许多类都得导入和使用它。当Error枚举修改时,所有这些其他的类都需要重新编译和部署。[11]这对Error类造成了负面压力。程序员不愿增加新的错误代码,因为这样他们就得重新构建和部署所有东西。于是他们就复用旧的错误码,而不添加新的。
别重复自己
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。
结构化编程
有些程序员遵循EdsgerDijkstra的结构化编程规则[15]。Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。遵循这些规则,意味着在每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
我们赞成结构化编程的目标和规范,但对于小函数,这些规则助益不大。只有在大函数中,这些规则才会有明显的好处。所以,只要函数保持短小,偶尔出现的return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。另外一方面,goto只在大函数中才有道理,所以应该尽量避免使用。
如何写出好的函数
写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。
小结
大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。那种领域特定语言的一个部分,就是描述在系统中发生的各种行为的函数层级。在一种狡猾的递归操作中,这些行为使用它们定义的与领域紧密相关的语言讲述自己那个小故事。