代码整洁之道,关于函数部分的总结
一、函数只做一件事
函数应该只做一件事、做好这件事、只做这件事。
判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新
二、函数尽量不要太长
按照作者的理论,函数长度20行封顶为最佳。
我的理解是,函数长度需要跟第一节的函数只做一件事结合起来,并不需要完全拘泥于20行的限制,只要函数在逻辑层次上不可再分解成新的函数(参见第三条),就可以。
三、每个函数一个抽象层次
要确保函数只做一件事,函数中的语句都要在同一抽象层级上。
我们想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。我把这叫做向下规则。
这里咱举个例子,加入你要对一个字符串进行一些处理(例如,append,substring等),然后再对处理后的字符串进行校验。
良好的实践应该类似如下:
String handledStr = handleStr(username);
validate(handledStr);
不是很优雅的实践类似如下:
String trimmedUserName = username.trim();
String handledStr = trimmedUserName.append("something");
validate(handledStr);
为什么说它不是最佳实践呢?因为前两行代码是关于对字符串进行处理的具体操作,应该把它们抽象成一个函数,这个抽象出来的函数是这两个具体操作的上一层概念,和validate方法同一层。
四、switch语句
写出精简的switch语句很难,写出只做一件事的switch语句也很难,它天生就要做N件事。我们无法避开switch语句,不过还是可以确保每个switch都放在较低的抽象层级,而且永远不重复。
利用多态实现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);
}
}
这里仅仅依赖了雇员类型一种操作,就有好几个问题:
- 函数太长,当有新的雇员类型,还会更长。
- 违反开闭原则(OCP原则),每添加新类型,就必须修改它。
- 违反了单一权责原则,它做了多件事情。
更麻烦的是:到处都有类似的调用函数(传入的参数类似)。
比如可能多处调用
isPayday(Employee e, Date date);
或
deliverPay(Employee e, Money pay);
下面我们针对这样的问题进行优化。
对每个类都会有同样的操作,比如isPayday(), deliverPay()等,不如把类的行为抽象出来到一个抽象类Employee中。在抽象工厂中使用switch语句为Employee的派生物创建适当的实体。
对于switch语句,我们的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在其他系统看不到,就还能容忍。
public abstract class Employee{
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory{
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory{
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType{
switch (r.type){
case COMMISSIONED:
return new CommissionedEmployee(r);
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmployee(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
用优化后的代码,再有新的类型加入时,业务程序是不用修改的,因为类型已隐藏在了抽象类中,返回给业务的都是抽象的Employee,无需考虑类型的变化,只是调用抽象类的方法即可。我们需要改动的只是再创建一个抽象类的实体类,在EmployeeFactoryImpl中多加一个switch分支。
五、使用描述性的名称
长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
六、函数参数
参数数量越少越好。 尽量不要有输出参数,而是将输出设置为返回值。 如果参数较多的时候可以考虑使用类进行封装。
七、无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出没有预料到的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
例如如下代码,改函数使用标准算法来匹配userName和passWord,如果匹配成功,返回true,如果匹配失败,返回false,但是它会有副作用:
副作用就在于对Session.initialize()调用。checkPassword函数,顾名思义,就是用来检查密码的。该名称并没有暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就会冒着抹除现有会话数据的风险。也就是说,这个副作用造成了一次时序性耦合。
八、分隔指令与询问
这实际上内生的包含于一个函数只做一件事的要求中,但是还是有必要单独指出。函数要么做什么事,要么回答什么事,但二者不可兼得。
函数应该修改某对象的状态,或者返回该对象的有关信息。两样都干会导致混乱。
举个例子:
if (set("username", "unclebob"))...
上面这个语句会让人迷惑:它是询问username属性之前是否已经被设置为unclebob了?还是在问username属性是否成功被设置为unclebob呢?从这行调用很难判断其含义。
要解决这个问题,可以按如下方式改造,防止混淆的发生:
if (attributeExists("username")) {
setAttribute("username", "unclebob");
}
九、使用异常替代返回错误码
可减少过度嵌套(判断多种错误码及内层错误码)。
可减少对错误码枚举类的过度依赖(当修改了错误码枚举类时,所有依赖这个枚举类的其他类都得重新编译和部署)。
十、别重复自己
如果你发现某两个函数用到了相同甚至相近的代码块应该迅速思考是不是可以将其抽取成单独的函数。 重复就是万恶之源。
十一、结构化函数
Dijkstra认为,每个函数、每个代码块都应该只有一个入口一个出口。这意味着每个函数只能有一个return语句,循环中不能有break或continue,而且永远不能出现goto。 事实上,当代码相对较短的时候,适当多几个return、break、continue无伤大雅。当代码冗长时,这样的规则才能够发挥出其效力来。
十二、如何写出好的函数?
写代码和写别的东西很像,在写文章或者论文时,你先想些什么就写什么,然后再打磨它。 初稿也许粗陋无序,你就斟酌推敲,直到达到你心中的样子。
作者也并不是一开始就遵照规则写函数,估计也没有人做得到。都是不断打磨,分解函数、修改名称、消除重复。
参考
《代码整洁之道》
网友评论