3.1、短小
- 函数20行封顶最佳。
- 每个函数都依序把你带到下一个函数。
- 函数的缩进层级不该多于一层或者两层。
3.2、只做一件事情
- 函数应该只做一件事,并且做好这件事。
- 判断函数只做一件事的准则就是不能再将该函数再细化
3.3 每个函数一个抽象层级
- 自顶向下读代码:向下规则
- 这个意思就是说,在国家这个层级下面就是省份,省份下面为市级,这样一层层关系明确,类似树形结构。
- 减少bug量的发生等。
3.4 switch 语句
写出短小的switch语句很难[28]。即便是只有两种条件的switch语句也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。
请看代码清单3-4。它呈现了可能依赖于雇员类型的仅仅一种操作。
代码清单3-4 Payroll.java
public Money calculatePay(Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
returncalculateCommissionedPay(e);
case HOURLY:
returncalculateHourlyPay(e);
case SALARIED:
returncalculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
该函数有好几个问题。
首先,它太长,当出现新的雇员类型时,还会变得更长。
其次,它明显做了不止一件事。
第三,它违反了单一权责原则(Single Responsibility Principle[29], SRP),因为有好几个修改它的理由。
第四,它违反了开放闭合原则(Open Closed Principle[30], OCP),因为每当添加新类型时,就必须修改之。
不过,该函数最麻烦的可能是到处皆有类似结构的函数。例如,可能会有
isPayday(Employee e, Date date),
或
deliverPay(Employee e, Money pay),
如此等等。它们的结构都有同样的问题。
该问题的解决方案(如代码清单3-5所示)是将switch语句埋到抽象工厂[31]底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则藉由Employee接口多态地接受派遣。
对于switch语句,我的规矩是如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其他部分看不到,就还能容忍[G23]。当然也要就事论事,有时我也会部分或全部违反这条规矩。
代码清单3-5 Employee与工厂
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 implementsEmployeeFactory {
public Employee makeEmployee(EmployeeRecord r)throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return newCommissionedEmployee(r) ;
case HOURLY:
return newHourlyEmployee(r);
case SALARIED:
return newSalariedEmploye(r);
default:
throw newInvalidEmployeeType(r.type);
}
}
}
}
3.5 使用描述性的名称
public void sendToQuotationServerStatus() { public void sendToQuotationJadeQuotations(String scode) {
public void noticQuotationServerSendQuotationData(String scode) {
public void sendToQuotationServerStatus() {
3.6 函数的参数
最理想的参数数量是0,其次是1,再次是2,尽量避免3.
不建议
makeCircle(double x, double y, double radius);建议makeCircle(Point center, double radius);
3.7 无副作用
副作用是一种谎言。函数承诺只做一件事情,但是还是会做其他被藏起来的事。
以代码清单3-6中看似无伤大雅的函数为例。该函数使用标准算法来匹配userName和password。如果匹配成功,返回true,如果失败则返回false。
但它会有副作用。你知道问题所在吗?
代码清单3-6 UserValidator.java
publicclass UserValidator { private Cryptographer cryptographer; public boolean checkPassword(String userName, String password) { User user = UserGateway.findByName(userName); if (user != User.NULL) { String codedPhrase = user.getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codedPhrase, password); if ("Valid Password".equals(phrase)) { Session.initialize(); return true; } } return false; } }
当然了,副作用就在于对Session.initialize( )的调用。checkPassword函数,顾名思义,就是用来检查密码的。
该名称并未暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。
这一副作用造出了一次时序性耦合。也就是说,checkPassword只能在特定时刻调用(换言之,在初始化会话是安全的时候调用)。
如果在不合适的时候调用,会话数据就有可能沉默地丢失。时序性耦合令人迷惑,特别是当它躲在副作用后面时。
如果一定要时序性耦合,就应该在函数名称中说明。
在本例中,可以重命名函数为checkPasswordAndInitializeSession,虽然那还是违反了“只做一件事”的规则。
3.8 分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。看看下面的例子:
public boolean set(String attribute, String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个属性则返回false。这样就导致了以下语句:
if (set("username", "unclebob"))...
从读者的角度考虑一下吧。这是什么意思呢?它是在问username属性值是否之前已设置为unclebob吗?或者它是在问username属性值是否成功设置为unclebob呢?从这行调用很难判断其含义,因为set是动词还是形容词并不清楚。
作者本意,set是个动词,但在if语句的上下文中,感觉它像是个形容词。该语句读起来像是说“如果username属性值之前已被设置为uncleob”,而不是“设置username属性值为unclebob,看看是否可行,然后……”。要解决这个问题,可以将set函数重命名为setAndCheckIfExists,但这对提高if语句的可读性帮助不大。真正的解决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists("username")) {
setAttribute("username","unclebob");
...
}
3.9 使用异常代替返回错误
Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。
public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) throwsException { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); }
3.10 别重复自己
回头仔细看看代码清单3-1,你会注意到,有个算法在SetUp、SuiteSetUp、TearDown和SuiteTearDown中总共被重复了4次。识别重复不太容易,因为这4次重复与其他代码混在一起,而且也不完全一样。这样的重复还是会导致问题,因为代码因此而臃肿,且当算法改变时需要修改4处地方。而且也会增加4次放过错误的可能性。
使用代码清单3-7中的include方法修正了这些重复。再读一遍那段代码,你会注意到,整个模块的可读性因为重复的消除而得到了提升。
3.11 结构化编程
3.12 如何写出这样的函数
3.13 小结
每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设计来描述那个系统的。函数是语言的动词,类是名词。这并非是退回到那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可怕的旧观念。其实这是个历史更久的真理。编程艺术是且一直就是语言设计的艺术。
大师级程序员把系统当作故事来讲,而不是当作程序来写。他们使用选定编程语言提供的工具构建一种更为丰富且更具表达力的语言,用来讲那个故事。那种领域特定语言的一个部分,就是描述在系统中发生的各种行为的函数层级。在一种狡猾的递归操作中,这些行为使用它们定义的与领域紧密相关的语言讲述自己那个小故事。
本章所讲述的是有关编写良好函数的机制。如果你遵循这些规则,函数就会短小,有个好名字,而且被很好地归置。不过永远别忘记,真正的目标在于讲述系统的故事,而你编写的函数必须干净利落地拼装到一起,形成一种精确而清晰的语言,帮助你讲故事。
代码清单3-7 SetupTeardownIncluder.java
packagefitnesse.html;
importfitnesse.responders.run.SuiteResponder;
importfitnesse.wiki.*;
publicclass 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");
}
}