函数是所有程序中的第一组代码
3.1 短小
- 函数的第一规则是要短小
- 函数的第二规则是比短小更短小
- 函数的第三规则是尽可能短小
3.1.1 Bad Example
- 下面这段代码已经是对之前一段要长的的多的代码的优化,那么为什么依旧是 Bad Example 呢?
- 因为它显然还不够短小,函数不应该大到足以容纳嵌套结构
- 函数的缩进层级不该多余一层或两层
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
boolean isTestPage = pageData.hasAttribute(“Test”);
if (isTestPage) {
WikiPage wikiPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
3.1.2 Good Example
- 下面这段代码将嵌套结构进行了精简,将属于同一抽象层的内容迁移出去,用一个函数包裹,之后在源函数中引入该函数
- 这样做的好处显而易见,当前函数需要表达的意思更专注于本身
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData)) {
includeSetupAndTeardownPages(pageData, isSuite);
}
return pageData.getHtml();
}
3.2 只做一件事
- 函数应该做一件事,做好这件事,只做这一件事
- 要判断函数是否不止做了一件事,就要看是否能再拆出一个函数
- 拆出的函数不能是单纯的实现
- 需要是改变了抽象层级的
3.3 每个函数一个抽象层级
- 要确保函数只做一件事,首先需要确保函数中的语句都在同一个抽象层级
- 要让代码拥有自顶向下的阅读顺序
- 让代码读起来像是一系列自顶向下的 TO 起头段落,是保持抽象层级协调一致的有效技巧
- 例如:要做 A ,则需要 B ,要做 B ,则需要 C
3.4 switch 语句
- 重构一书中提到 “在面向对象中应该尽量少使用 switch 结构,从本质上来说,switch 结构的弊端在于重复,而多态是 switch 结构的优雅解决方式”
3.4.1 Bad Example
- 下面这段代码太长了,不够短小
- 还做了很多事情,违反了 单一权责原则( Single Responsibility Principle )SRP
- 每当添加新类型,就要修改函数本身,违反了 开放闭合原则( Open Closed Principle )OCP
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);
}
}
3.4.2 Good Example
- 下面这段代码用工厂模式和多态让代码结构可清晰,也更容易扩展
public abstract class Employee {
public abstract boolean isPalyDay();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord record) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord record) throws InvalidEmployeeType {
switch(record.type) {
case COMMISSIONED:
return new CommissionedEmployee(record);
case HOURLY:
return new HourlyEmployee(record);
case SALARIED:
return new SalariedEmployee(record);
default:
throw new InvalidEmployeeType(e.type);
}
}
}
3.5 使用描述性的名称
- 别害怕长名称,长而具有描述性的名称,要比短而令人费解的名城好,要比描述性的长注释好
3.6 函数参数
- 最理想的参数数量是没有参数,也可以叫 零参数函数 、无参函数
- 其次是单参数函数
- 再次是双参数函数
- 如果一个函数有三个甚至更多参数,就说明这个函数必须得优化了
- 从测试的角度来看待,参数越多越让人为难,因为测试用例的编写需要考虑的前提条件过多
3.6.1 一元函数的普遍形式
- 往函数中传入参数的目的一般就两种理由
- 操作函数
- 转换形式
- 应当选用能区别这两种理由的名称,而且只能在一致的上下文中使用这两种形式
- 如果函数要对输入参数进行转换操作,转换结果就应该体现为返回值
StringBuffer transform(String in)
就比void transform(StringBuffer in)
要更直观的表达函数的作用
3.6.2 标识参数
- 向函数中传入布尔值简直就是骇人听闻的做法
- 这相当于就是在大肆宣扬这个函数不止做了一件事,它可能做这件事,也可能做那件事
- 针对于需要传入布尔值参数的函数,应该在该函数一分为二的表达出来
3.6.3 二元函数
- 有两个参数的函数要比一元函数难懂,因为要花更多的精力去理解传入参数对于这个函数的作用
- 但二元函数是普遍存在的,只是在编写函数时,应当优先考虑一元函数,如果实在无法解决,在考虑使用二元函数
3.6.4 三元函数
- 有三个参数的函数要比二元函数更难懂,跟一元函数则更没有可比性
- 其实当传入参数达到三个甚至更多时,就需要考虑这些参数是否可以封装为一个对象
3.6.5 参数对象
- 如果函数看起来需要两个、三个或者三个以上参数
- 就说明其中一些参数应该封装为类
3.6.5.1 Bad Example
Circle makeCircle(double x, double y, double radius);
3.6.5.2 Good Example
- 将 x 和 y 封装为一个 Point 类,是因为得知 x 和 y 的位置后,就可以确定一个点的所在
- 说明这两个参数之间确实是可以找到关联的
Circle makeCircle(Point point, double raduis);
3.6.6 参数列表
- 有 可变参数 的函数可能是一元、二元甚至三元,活着更多
- 但可变参数本身在定义时,完全可以作为一个参数对待
- 下面这段代码是
String.format()
的定义,就用到了可变参数
public String format(String format, Object... orgs);
3.6.7 动词于关键字
- 对于一元函数,函数和参数应当形成一种非常良好的 动词/名词 组合形式
write(name)
表示要写入一个名称writeFiled(name)
表示要写入一个字段的名称
3.7 无副作用
- 函数承诺只做一件事,但还是会做其他被隐藏起来的事,导致古怪的时序性耦合及顺序依赖
- 如果编写一个函数时,没有通过名称或者注释清晰的表达函数将要做的事,那么就会导致某些函数实际上要做的事被隐藏
3.7.1 输出参数
- 普遍而言,应避免使用输出参数,要么使用返回值,要么直接修改所属对象的状态
- 例如
appendFooter(Report report)
应该写成report.appendFooter()
3.8 分隔指令和询问
- 函数要么做什么事,要么回答什么事,不能同时把两件事都做了
3.8.1 Bad Example
- 下面这段代码想要表达的是为某个属性赋值,同时返回一个结果
- 但你无法确定返回的这个结果是什么
- 有可能是,该属性是否已经设置该值
- 又或者是,该属性是否成功设置该值
public boolean set(String attribute, String value);
3.8.2 Good Example
- 如果一个函数可能会做两件事,那么将这个函数直接改成两个函数,是最好的优化办法
public boolean attributeExists(String attribute);
public void setAttribute(String attribute, String value);
3.9 使用异常替代返回错误码
- 从指令函数返回错误码,违反了指令于询问分隔的规定,因为这表示 指令在 if 语句中被当作表达式使用
3.9.1 Bad Example
- 下面这段代码就是 从指令函数返回错误码 的极致表现
if (deletePage(page) == OK) {
if (registry.deleteKey(page.name) == OK) {
if (configKeys.deleteKey(page.name.makeKey()) == OK) {
logger.log(“page deleted”);
} else {
logger.log(“configKey not deleted”);
return ERROR_CODE;
}
} else {
logger.log(“deleteReference form registry failed”);
return ERROR_CODE;
}
} else {
logger.log(“delete failed”);
return ERROR_CODE;
}
3.9.2 Good Example
- 如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,代码将得到简化
try {
deletePage(page);
registry.deleteReference(page.name);
configKey.deleteKey(page.name.makeKey());
} catch (Exception e) {
logger.log(e.getMessage());
}
3.9.3 抽离 try/catch 代码块
- 最好把 try/catch 代码块的主体部分抽离出来,另外形成函数
- 这样业务逻辑本身就可以专注处理自己的事情,把可能抛出的错误丢给上层去处理
- 而上层函数也只需要处理可能抛出的错误,而不需要顾及真实的业务逻辑
public void delete(Page page) {
try {
deletePageAndAllReference(page);
} catch(Exception e) {
logger.log(e.getMessage());
}
}
3.9.4 错误处理就是一件事
- 处理错误的函数不该做其他事,所以代码应该像上述例子一样进行优化
3.9.5 Error.java 依赖磁铁
- 其实就是指一般在项目中都会创建一个错误代码的枚举类
- 枚举类看似简洁,但是如果修改修改枚举时,调用了这个枚举的所有引用都需要修改
- 使用异常替代错误码,新异常就可以错异常类派生出来,无需重新编译或修改原始业务逻辑
3.10 别重复自己
- 重复可能是软件中一切邪恶的根源
- 软件开发领域的所有创建都是在不断尝试从源代码中消灭重复
- 所以我们在编写代码时,更不能够去创造重复
3.11 结构化编程
- 每个函数中只能有一个 return 语句
- 不认同,根据不同情况返回不同的结果应该是被允许的
- 循环中不能有 break 或 continue 语句
- 不认同,根据不同情况去结束或跳过循环,显然可以提高效率
- 永远不能有任何 goto 语句
- 认同
3.12 如何写出这样的函数
- 好的函数是一个逐步优化的过程,遵循上面提到的优化准则,则更有助于写出好的函数
3.13 小结
- 函数是语言的动词,类是名词
- 编程艺术是且一直就是语言设计的艺术