第三章 函数
1. 短小
- 函数的第一条规则是短小,20行封顶最佳;
- if, else, while 语句中的代码块应该只占一行(函数调用语句);
- 函数不应该大到足以容纳嵌套结构,缩进层级不该多于两层。
2. 只做一件事
- 函数应该只做一件事,并做好这件事;
- 判断函数是否不止做了一件事:能否再拆出一个函数。
3. 每个函数一个抽象层级
- 函数中混杂不同抽象层级,往往会让人迷惑,无法判断某个表达式是基础概念还是细节;
- 破窗理论:一旦细节与基础概念混杂,更多的细节就会在函数中纠结起来;
- 让代码拥有自顶向下的阅读顺序,每个函数后面都跟着位于下一抽象层级的函数,每一层保持相同抽象层级;
4. switch语句
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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); } }
|
问题:
太长,并且会增长(出现新类型时);
违反了单一权责原则;
违反了开放闭合原则,添加新类型时,必须修改函数;
连锁反应导致出现大量类似结构的函数:
1 2 3
| isPayday(Employee e, Date date);
deliverPay(Employee e, Money pay)
|
解决方案:
将switch语句埋藏到抽象工厂底下。
该工厂使用 switch 语句为 Employee 的派生物创建适当的实体,而不同的函数,如calculatePay, isPayday 和 deliverPay 等,则借由Employee接口多态地接受派遣。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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) throw 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(e.type); } } }
|
5. 使用具有描述性的名称
- 别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好;
- 别害怕花时间起名字;
- 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如:
includeSetupAndTeardownPages
,
includeSetupPages
,
includeSuiteSetupPage
和
includeSetupPage
等。
6. 函数参数
6.1 单参数函数的普遍形式
两种普遍形式:
asking question:
1
| boolean fileExists("myFile")
|
operating:
1
| InputStream fileOpen("MyFile")
|
不普遍但有用:
6.2 标识参数——丑陋不堪
- 标识参数丑陋不堪,方法签名变得复杂,并意味着函数不止做一件事儿。
6.3 双参数函数
尽量利用一些机制将其转换成单参数函数: writeField(outputStream, name)
- 将
writeField
方法写成outputStream
的成员之一:outputStream.writeField(name)
;
- 把
outputStream
写成当前类的成员变量,从而无需再传递它;
- 分离出类似于
FieldWriter
的新类,在其构造器中采用outputStream
6.4 三参数函数
考虑清楚!!!
6.5 参数对象
如果函数看起来需要2个、3个或3个以上参数,就说明其中一些参数应该封装为类了。
或,当一组参数被共同传递(x, y),往往就该被封装为类。
6.6 参数列表
可变参数
6.7 动词与关键字
7. 无副作用
副作用是一种谎言。函数只做一件事,但偶尔会做其他被藏起来的事儿:
- 对自己类中的变量做出未能预期的改动;
- 把变量搞成向函数传递的参数或是系统全局变量。
避免使用输出参数:
1
| public void appendFooter(StringBuffer report)
|
转换为:
结论:如果函数必须要修改某种状态,就修改所属对象的状态。
8.分隔指令与询问
函数要么做什么事,有么回答什么事,二者不可兼得。
- 修改某对象状态(指令);
- 返回该对象的有关信息(询问);
9.使用异常替代返回错误码
从指令函数返回错误码略微违反了指令与询问分隔的原则,并引起更深层次的嵌套结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if (deletePage(page) == E_OK) { if (registry.deleteReference(page.name) == E_OK) { if (configKeys.deleteKey(page.name.makeKey()) == E_OK){ logger.log("page deleted"); } else { logger.log("configKey not deleted"); } } else { logger.log("deleteReference from registry failed"); } } else { logger.log("delete failed"); return E_ERROR; }
|
使用异常替代返回错误码,错误处理代码能从主路径代码中分离出来:
1 2 3 4 5 6 7 8
| try { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } catch (Exception e) { logger.log(e.getMessage()); }
|
9.1 抽离 try/catch 代码块
try/catch
代码块丑陋不堪。他们搞乱了代码结构,把错误处理与正常流程混为一谈。
最好把 try
和catch
代码块的主体部分抽离出来,另外形成函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } }
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()); }
|
9.2 错误处理就是一件事儿
函数应该只做一件事儿——错误处理就是一件事儿。
如果关键字try在某个函数中存在,它就应该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容.
9.3 Error.java 依赖磁铁
错误类/枚举就是一块依赖磁铁,其他许多类都得导入和使用它。当枚举修改时,其他所有类都需要重新编译和部署。
1 2 3 4 5 6 7 8
| public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }
|
解决:
使用异常替代错误码,新异常就可以从异常类派生出来,而无需重新编译和部署。
参见第七章:错误处理
10. 别重复自己
重复可能是软件中一切邪恶的根源。
后记1:如何写出这样的函数
写代码和写别的东西很像。在写论文或文章时,先是想到什么就写什么,然后再打磨它。初稿也许粗陋无序,可以对其斟酌推敲,直至达到你心中的样子。
打磨代码,分解函数,修改名称,消除重复.
总结:编程是门语言艺术!!!不能太懒!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| package fitnesse.html;
import fitnesse.responders.run.SuiteResponder; import fitnesse.wiki.*;
public class SetupTeardownIncluder { private PageData pageData; private boolean isSuite; private WikiPage testPage; private StringBuffer newPageContent; private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception { return render(pageData, false); }
public static String render(PageData pageData, boolean isSuite) throws Exception { return new SetupTeardownIncluder(pageData).render(isSuite); }
private SetupTeardownIncluder(PageData pageData) { this.pageData = pageData; testPage = pageData.getWikiPage(); pageCrawler = testPage.getPageCrawler(); newPageContent = new StringBuffer(); }
private String render(boolean isSuite) throws Exception { this.isSuite = isSuite; if (isTestPage()) includeSetupAndTeardownPages(); return pageData.getHtml(); }
private boolean isTestPage() throws Exception { return pageData.hasAttribute("Test"); }
private void includeSetupAndTeardownPages() throws Exception { includeSetupPages(); includePageContent(); includeTeardownPages(); updatePageContent(); }
private void includeSetupPages() throws Exception { if (isSuite) includeSuiteSetupPage(); includeSetupPage(); }
private void includeSuiteSetupPage() throws Exception { include(SuiteResponder.SUITE_SETUP_NAME, "-setup"); }
private void includeSetupPage() throws Exception { include("SetUp", "-setup"); }
private void includePageContent() throws Exception { newPageContent.append(pageData.getContent()); }
private void includeTeardownPages() throws Exception { includeTeardownPage(); if (isSuite) includeSuiteTeardownPage(); }
private void includeTeardownPage() throws Exception { include("TearDown", "-teardown"); }
private void includeSuiteTeardownPage() throws Exception { include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown"); }
private void updatePageContent() throws Exception { pageData.setContent(newPageContent.toString()); }
private void include(String pageName, String arg) throws Exception { WikiPage inheritedPage = findInheritedPage(pageName); if (inheritedPage != null) { String pagePathName = getPathNameForPage(inheritedPage); buildIncludeDirective(pagePathName, arg); } }
private WikiPage findInheritedPage(String pageName) throws Exception { return PageCrawlerImpl.getInheritedPage(pageName, testPage); }
private String getPathNameForPage(WikiPage page) throws Exception { WikiPagePath pagePath = pageCrawler.getFullPath(page); return PathParser.render(pagePath); }
private void buildIncludeDirective(String pagePathName, String arg) { newPageContent .append("\n!include ") .append(arg) .append(" .") .append(pagePathName) .append("\n"); } }
|