整洁代码之道 3 函数

mac2026-09-22  8

函数是所有程序中的第一组代码

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 小结

函数是语言的动词,类是名词编程艺术是且一直就是语言设计的艺术
最新回复(0)