2.函数(代码的整洁之道)
目录
- 短小
- 只做一件事
- 每个函数一个抽象层次
- switch语句
- 使用描述性的名称
- 函数参数
- 无副作用
- 分隔指令与询问
- 使用异常代替返回的错误码
- 别重复自己
- 结构化编程
- 如何写出这样的函数
- 小结
注:代码的整洁之道PDF: https://pan.baidu.com/s/16PLDWPiusGjcUfW_jgOm5w 密码: s708
1. 短小
- 函数的第一规则是要短小,第二条规则还是要更短小。
- 每个函数都只说一件事,而且每个函数会依序把你带到下一个函数。
- 函数应该有多短小?应该缩短成下面代码的样子。
- if语句、else语句、while语句等,其中的代码应该只有一行,该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且因为块内调用的函数拥有较具体说明性的名称,从而增加了文档上的价值。
- 这也意味着函数不应该大到足够容纳嵌套结构。函数的缩进层不该多余一层或两层。
2. 只做一件事
- 函数应该只做一件事,做好这件事。只做这一件事。
- 编写函数是为了把大一些的概念拆分成另一抽象层上的一系列步骤。
- 自顶向下规则,让每个函数后面都跟着位于下咦抽象层级的函数,在查看函数列表时,就能遵循抽象层级向下阅读了。
3. 每个函数一个抽象层次
- 要确保函数只做一件事,函数中的语句都要在同一抽象层级上
4. switch语句
- 写出短小的switch语句很难,及时只有两种条件也要比单个代码块或函数大得多。
- switch存在几个问题
- 它太长,每当出现新的类型,还会变得更长
- 其实,明显不止做了一件事。
- 违反了单一权责原则
- 违反了开放闭合原则,每当添加新类型,就必须修改。
- 最麻烦的是可能导出皆有类似结构的函数。
- 该解决方案是将switch语句埋到抽象工厂下,不让任何人看到。改工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday和deliverPay等,则由Employee接口多态地接受派遣。
- 对于switch语句,如果只出现一次,用于创建多态对象,而且隐藏在某个继承关系中,在系统其它部分看不到,就还能容忍。
5. 使用描述性的名称
- 如果每个例程都让你感到深合已意,就是整洁代码。
- 函数越短小,功能越集中,就越便于取个好名字。
- 别害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好。
- 命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPages、includeSetupPages、includeSuiteSetupPage和includeSetupPage等。这些词使用了类似的措辞,依序讲出一个故事。
6. 函数参数
- 最理想的参数数量是零,其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。
- 参数带有太多的概念性,参数与函数名处在不同的抽象层级,要求你了解目前并不特别重要的细节。
1. 一元函数的普遍形式
- 像在boolean fileExists(“MyFile”)中那样,可能是操作该参数,将其转换为其他说明东西,再输出。例如,InputStream fileOpen(“MyFile”)把String类型的文件名转换为InputStream类型的返回值,就是读者看到函数时所期待的东西。你应该选用较能区别这两种理由的名称,而且总能在一致的上下文中使用这两种形式。
- 还有一种就是事件。在这种形式中,有输入参数而无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态,例如 void passowrdAttemptFailedNtimes(int attempts)。小心使用这种形式,应该让读者很清楚的了解它是个事件。谨慎的选用名称与上下文语境。
- 对于转换,使用输出参数而非返回值令人迷惑。**如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。**实际上,StringBuffer transform(StringBuffer in)要比void transform(StringBuffer out)强,即便第一种形式只简单地返回输出参数。
2. 标识参数
- 标识参数丑陋不堪,向函数传入布尔值简直就是骇人听闻,表示函数不止做一件事,如果标识为true将这么做,标识为false将那样做。
- 看到render(Boolean isSuite),应该把函数一分为二:renderForSuite()和renderForSingleTest()
3. 二元函数
- 有两个参数的函数要比一元函数难懂。例如,writeField(name)要比writeField(outputStream,name)好懂。
- 你应该尽量利用一些机制将其转换成一元函数,例如,可以把writeField方法写成outputStream的成员之一,从而能这样用:outputStream.writeField(name),也可以把outputStream写成当前类的成员变量,从而无需再传递它。还可以分离出类似FieldWriter的新类,在其构造器中采用outputStream,并包含一个write方法。
4. 三元函数
- 有三个参数的函数要比二元函数难懂很多。
5. 参数对象
- 如果函数看来需要两个、三个或以上的参数,就说明其中一些参数应该封装成类了。
- 例如,下面两个声明的差别
- Circle makeCircle(double x, double y, double radius)
- Circle makeCircle(Point center, double radius)
- 从参数创建对象,从而减少参数数量,当一组参数被共同传递,就像上面的x和y,往往就是该有自己名称的某个概念的一部分。
6. 动词与关键词
- 给函数取个好名字,能较好解释函数的意图,以及参数的顺序和意图。
- 对于一元函数,函数和参数应该形成良好的动词/名词对形式,例如,write(name)就相当令人认同。
- 这个例子展示了函数名称的关键字形式。使用这种形式,我们把参数的名称编码成了函数名,例如,assertEqual改成assertedEqualsActual(expected, actual)可能会好些。
7. 无副作用
- 函数承诺只做一件事,,但有时会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或者是系统的全局变量。无论哪种情况,会导致古怪的时序性耦合以及顺序依赖。
- 如下面代码
- 副作用在于Session.initialize()的调用。checkPassword函数,顾名思义,是用来检查密码的,该名称并未暗示它会初始化该次会话。
- 这一副作用造出了一次时序性耦合,也就是说,checkPassword只能在特定的时刻调用。
- 在本例中,可以重命名函数为 checkPasswordAndInitializeSession,虽然还是违反了只做一件事原则。
1. 输出参数
- 面向对象语言中对输出参数的大部分需求已经消失了,换言之
- public void appendFooter(StringBuffer report)最好改成 report.appendFooter()
8. 分隔指令与询问
- 函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干会导致混乱。比如:
- public boolean set(String attribute, String value);
- 该函数设置了某个指定属性,如果成功就返回true,如果不存在那个属性则返回false。这就导致了以下语句
if (set("username", "unclebob"))
- 从if调用很难判断其含义,因为set是动词还是形容词并不清楚。可将set函数重命名为setAndCheckIfExists,但这对提高if语句的可读性帮助不大。
- 真正解决方案是把指令与询问分隔开来,防止混淆的发生:
if (attributeExists("username")){
setAttribute("username", "unclebob")
}
....
9. 使用异常代替返回的错误码
- 从指令式函数返回错误码轻微违反了指令与询问分隔的规则,它鼓励在if语句判断中把指令当做表达式使用。
if (deletePage(page)== E_OK)
- 这不会引起动词/形容词混淆,但却导致更深层次的嵌套结构。
- 如果使用异常代替返回错误码,错误处理就能出主路径代码中分离出来,得到简化。
1. 抽离 Try/Catch 代码块
- try/catch代码块丑陋不堪,搞乱了代码结构,最好把try/catch代码块的主体部分抽离出来,另外形成函数。
- 上例中,delete函数只与错误处理有关。
10. 别重复自己
- 重复的代码会导致臃肿,并且需要修改的地方在增加,增加了错误的可能性。
11. 结构化编程
12. 如何写出这样的函数
- 刚开始写函数时,会冗长和复杂,但要打磨这些代码,分解函数、修改名称、消除重复。缩短和重新安置方法,有时还拆散类。
13. 小结
- 代码赏析
public class HtmlUtilCleanCode {
private WikiPage testPage;
private StringBuffer newPageContent;
private boolean isSuiteSetup;
private PageCrawler pageCrawler;
private PageData pageData;
public static String reader(PageData pageData) {
return reader(pageData, false);
}
public static String reader(PageData pageData, boolean isSuite) {
return (new HtmlUtilCleanCode(pageData)).reader(isSuite);
}
private HtmlUtilCleanCode(PageData pageData) {
this.testPage = pageData.getwikiPage();
this.pageCrawler = this.testPage.getPageCrawler();
this.newPageContent = new StringBuffer();
}
private String reader(boolean isSuite) {
this.isSuiteSetup = isSuite;
if (this.isTestPage()) {
this.includSetupAndTearPages();
}
return this.pageData.getHtml();
}
private boolean isTestPage() {
return this.pageData.hasAttribute("test");
}
private void includSetupAndTearPages() {
this.includeSetupPages();
this.includePageContent();
this.includeTearPages();
}
private void includeSetupPages() {
if (this.isSuiteSetup) {
this.include("SuiteResponder.SUITE_SETUP_NAME", "-setup");
}
this.includeSetupPage();
}
private void includeSetupPage() {
this.include("SetUp", "-setup");
}
private void includePageContent() {
this.newPageContent.append(this.pageData.getContent());
}
private void includeTearPages() {
if (this.isSuiteSetup) {
this.include("SuiteResponder.SUITE_SETUP_NAME", "-teardown");
}
this.includeTearPage();
}
private void includeTearPage() {
this.include("TearDown", "-teardown");
}
private void include(String pageName, String args) {
WikiPage innerPage = PageCrawlerImpl.getInheritedPage(pageName, this.testPage);
if (innerPage != null) {
WikiPagePath pagePath = innerPage.getPageCrawler().getFullpath(innerPage);
String pagePathName = PathParser.render(pagePath);
this.newPageContent.append("!include -").append(args).append(".").append(pagePathName).append("\n");
}
}
}