文章目录
短小
- 函数第一规则 要短小
- 函数第一规则 还要更短小
通常要小于 10 行
if | else | while 语句,其中的代码块应该只有一行 这行大抵应该是个函数调用语句 - 不但能保持短小
- 函数名较具有说明性 增加了文档价值
=> 函数不应大到足以容纳嵌套结构 函数缩进层级不该多于一层或两层
只专心做一件事
函数应该只做好一件事
判断函数是否不止做了一件事 ?
- 判断几步是否都在该函数名下同一抽象层上
- 看能否再拆出一个函数 该函数不仅只是单纯地重新诠释其实现
每个函数一个抽象层级
函数中语句都要在同一抽象层次上 让每个函数后面都跟着位于下一抽象层级的函数【从上而下自大而小的递归顺序】
写出只停留于一个抽象层次上的函数 => 是保持函数短小 确保只做一件事的要诀
swith
确保每个 swich 都在较低的抽象层级 而且永不重复
public Money calculatePay(Employee e) throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return caclculateCommissionedPay(e);
case HOURLY:
return caclculateHourlyPay(e);
case SALARIED:
return caclculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
问题
- 太长 出现新类型时 还会更长
- 不止做了一件事
- 违反了单一权责原则 —— 有好几个修改它的理由
- 违反了开放闭合原则 —— 每当添加新类型时 就必须修改之
- 最麻烦的:可能到处都有类似结构的函数 calculatePay | isPayday | deliverPay
- 解决:埋到抽象工厂下 不让人看到
单一职责原则: 就一个类而言,应该仅有一个引起它变化的原因。
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 {
switch (e.type) {
case COMMISSIONED:
return caclculateCommissionedPay(e);
case HOURLY:
return caclculateHourlyPay(e);
case SALARIED:
return caclculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
使用描述性名称
函数越短小 功能越集中 就越便于取个好名字
长而具有描述性的名称 比描述性的长注释好
命名方式保持一致
函数参数
0 参 > 1 参 > 2 参 避免 3 参 有足够特殊的理由才能用三个以上参数
如果参数多余 2 个 测试覆盖所有可能值的组合简直让人生畏
一元函数
- 转换:有入有出 出入相关
- 事件:有入无出
void includeSetupPageInfo(StringBuffer pageText)
避免使用输出参数 如果函数要对输入参数进行转换 转换结果就该提现为返回值
void transform(StringBuffer out)
=>
StringBuffer transform(StringBuffer in)
标识函数
标识参数丑陋不堪 向函数传入布尔值简直就是骇人听闻的做法 方法签名立刻变得复杂起来 大声宣布函数不止做一件事 —— 一分为二!!!
render(true) 看不懂做了什么
render(Boolean isSuite) => renderForSuite() | renderForSingleTest()
二元函数
笛卡尔点
特点:自然组合 | 自然排序
问题:顺序搞不清 e.g. assetEquals(expected, actual)
二元转一元:方法写成其中一个参数的成员
e.g. writeField(outputStream, name) => outputStream.writeField(name)
也可以把 outputStream 写成当前类的成员变量,从而无需再传递它
还可以分离出 FieldWriter 新类 构造器包含 outputStream 包含一个 write 方法
三元函数
排序 琢磨 忽略 问题会加倍体现,使用前一定要想清楚
参数对象
如果函数需要 2 个、3 个,3+个参数,就说明其中一些参数应该封装为类了
Circle makeCircle(double x, double y, double radius);
=>
Circle makeCircle(Point center, double radius);
参数列表
String.format 其实是个二元函数
动词与关键字
一元函数:函数名和参数应当形成一种非常良好的动词/名词对形式
write(name) => writeField(name) // name 是一个 Field
assetEqual => assetExpectedEqualsActual(expected, actual)
无副作用
时序性耦合 可能会在函数中悄悄执行其它的事情 这些事情应该在函数名称中说明 虽然还是违反了只做一件事的原则
checkPassword【函数中有执行方法 Sesson.initialize() 】 => checkPasswordAndInitializeSession
应避免使用输出参数 如果函数必须要修改某种状态 就修改所属对象的状态吧
分隔查询与指令
函数要么做什么事(修改某对象的状态) 要么回答什么事(返回该对象的有关信息) 两者不可兼得
boolean set(A, B)
=>
boolean setAndCheckIfExists()
=>
if (exists) {
setXxx(A, B)
}
使用异常代替返回错误码
从指令式函数返回错误码违反了指令与询问分隔的规则 => 如 if 语句判断中把指令当做表达式使用
if (deletePage(page) == E_OK) 不要这样用
如果使用异常替代返回错误码 错误处理代码就能从住路径代码中分离出来 得到简化
抽离 try-catch 代码块
try-catch 把错误处理与正常流程混为一谈 —— 最好抽离
- try-catch 函数
- throws 函数
// 错误处理代码 => 负责 catch 和处理
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
} catch (Exception e) {
logError(e);
}
}
// 业务功能代码 => 完全删除一个 page,方法层面向上抛出错误
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
错误处理就是一件事
如果关键词 try 在某个函数中存在 它就该是这个函数的第一个单词 而且在 catch / finally 后也不该有其他内容
如上例的 delete 函数
Error.java 依赖磁铁
返回错误码 => 类 / 枚举 定义了所有错误码 => 依赖磁铁 许多类都得导入和使用它
使用异常替代错误码 新异常就可以从异常类派生出来 无需重新编译或重新部署
别重复自己
重复使代码臃肿 => 重复消除 => 整个模块可读性提升
消除重复
- 数据库范式
- 基类——子类
- 面向方面编程
- 面向组件编程
结构化编程
适用大函数:每个代码块都应该有一个入口 一个出口 每个函数只该有一个 return 语句 循环中不能有 break 或 continue 语句
对于小函数:只要保持短小 偶尔出现的 break return continue 没有坏处
如何写出这样的函数
打磨
- 分解函数 —— 大=>小
- 修改名称 —— 使函数名能精确表达所做之事
- 消除重复 —— 抽出共有方法
- 缩短和重新安置方法 —— 重构
- 拆散类
- 保证测试通过
组装
不一开始就按照规则写函数 因为没人做得到
经典范例
作者这种分发我感觉有点极端了 复用性低 功能相近的函数我觉得可以整合在一个函数内 大概能省略将近一半的函数
最终的函数大小大概在 5~10 行
package fitness.html;
import fitness.responders.run.SuiteResponder;
import fitness.wiki.*;
public class SetupTearDownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
// 抽象层级[0] 渲染页面数据
public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}
// 抽象层级[1]
private static String render(PageData pageData, boolean isSuite) {
return new SetupTearDownIncluder(pageData).render(isSuite);
}
// 抽象层级[2]
public SetupTearDownIncluder(PageData pageData) {
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}
// 抽象层级[2]
private String render(boolean isSuite) {
this.isSuite = isSuite;
if (isTestPage()) {
includeSetupAndTeardownPages();
}
return pageData.getHtml();
}
// 抽象层级[3] 将if语句中的判断改为一个方法名描述 这样代码的可读性会上升 尤其是对于复杂的条件判断逻辑
private boolean isTestPage() {
return pageData.hasAttribute("Test");
}
// 抽象层级[3] 一个函数中的所有方法都是平级的 在下一级进行展开
private void includeSetupAndTeardownPages() {
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
// 抽象层级[4]
private void includeSetupPages() {
if (isSuite)
includeSuiteSetupPages();
includeSetupPage();
}
// 抽象层级[5]
private void includeSuiteSetupPages() {
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
// 抽象层级[5]
private void includeSetupPage() {
include("Setup", "-setup");
}
// 抽象层级[4]
private void includePageContent() {
newPageContent.append(pageData.getContent());
}
// 抽象层级[4]
private void includeTeardownPages() {
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
// 抽象层级[5]
private void includeTeardownPage() {
include("TearDown", "-teardown");
}
// 抽象层级[5]
private void includeSuiteTeardownPage() {
include(SuiteResponder.SUITE_SETUP_NAME, "-teardown");
}
// 抽象层级[4]
private void updatePageContent() {
pageData.setContent(newPageContent.toString());
}
// 抽象层级[6]
private void include(String pageName, String arg) {
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}
// 抽象层级[7]
private WikiPage findInheritedPage(String pageName) {
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
// 抽象层级[7]
private String getPathNameForPage(WikiPage page) {
WikiPagePath pagePath = pageCrawler.getFullPath(page);
return PathParser.render(pagePath);
}
// 抽象层级[7]
private void buildIncludeDirective(String pagePathName, String arg) {
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
}
// 其它辅助类 lombok get set 注解已省略
public class SuiteResponder {
public static final String SUITE_SETUP_NAME = "";
}
public interface PageCrawler {
WikiPagePath getFullPath(WikiPage page);
}
public class PageCrawlerImpl implements PageCrawler {
public static WikiPage getInheritedPage(String pageName, WikiPage testPage) {
return null;
}
public WikiPagePath getFullPath(WikiPage page) {
return null;
}
}
public class PageData {
private WikiPage wikiPage;
public boolean hasAttribute(String test) {
return false;
}
public String getHtml() {
return null;
}
public char[] getContent() {
return new char[0];
}
public void setContent(String str) {
}
}
public class PathParser {
public static String render(WikiPagePath pagePath) {
return null;
}
}
public class WikiPage {
private PageCrawler pageCrawler;
}
public class WikiPagePath {
}