第三章函数

第三章函数

// 代码清单 1
public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
    
    
    WikiPage wikiPage = pageData.getWikiPage();
    StringBuffer buffer = new StringBuffer();
    if (pageData.hasAttribute("Test")) {
    
    
        if (includeSuiteSetup) {
    
    
            WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(
                    SuiteResponder.SUITE_SETUP_NAME, wikiPage);
            if (suiteSetup != null) {
    
    
                WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
                String pagePathName = PathParser.render(pagePath);
                buffer.append("!include -setup .").append(pagePathName).append("\n");
            }
        }
        WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
        if (setup != null) {
    
    
            WikiPagePath setupPath = wikiPage.getPageCrawler().getFullPath(setup);
            String setupPathName = PathParser.render(setupPath);
            buffer.append("!include -setup .").append(setupPathName).append("\n");
        }
    }
    buffer.append(pageData.getContent());
    if (pageData.hasAttribute("Test")) {
    
    
        WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
        if (teardown != null) {
    
    
            WikiPagePath tearDownPath = wikiPage.getPageCrawler().getFullPath(teardown);
            String tearDownPathName = PathParser.render(tearDownPath);
            buffer.append("\n")
                    .append("!include -teardown .")
                    .append(tearDownPathName)
                    .append("\n");
        }
        if (includeSuiteSetup) {
    
    
            WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(
                    SuiteResponder.SUITE_TEARDOWN_NAME,
                    wikiPage);
            if (suiteTeardown != null) {
    
    
                WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown);
                String pagePathName = PathParser.render(pagePath);
                buffer.append("!include -teardown .")
                        .append(pagePathName)
                        .append("\n");
            }
        }
    }
    pageData.setContent(buffer.toString());
    return pageData.getHtml();
}

重构后

// 代码清单 2
public static String renderPageWithSetupsAndTeardowns(
        PageData pageData, boolean isSuite) throws Exception {
    
    
    boolean isTestPage = pageData.hasAttribute("Test");
    if (isTestPage) {
    
    
        WikiPage testPage = 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 短小

函数的第一规则是要短小。第二条规则是还要更短小。函数也不该有100行那么长,20行封顶最佳。
每个函数都一目了然。每个函数都只说一件事。而且,每个函数都依序把你带到下一个函数。这就是函数应该达到的短小程度!

// 代码清单 3
public static String renderPageWithSetupsAndTeardowns(
        PageData pageData, boolean isSuite) throws Exception {
    
    
    if (isTestPage(pageData))
        includeSetupAndTeardownPages(pageData, isSuite);
    return pageData.getHtml();
}

if语句、else语句、while语句等,其中的代码块应该只有一行。该行大抵应该是一个函数调用语句。这样不但能保持函数短小,而且,因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。
这也意味着函数不应该大到足以容纳嵌套结构。所以,函数的缩进层级不该多于一层或两层。当然,这样的函数易于阅读和理解。

3.2 只做一件事

函数应该做一件事。做好这件事。只做这一件事。问题在于很难知道那件该做的事是什么。代码清单3只做了一件事,对吧?其实也很容易看作是三件事:

  • 判断是否为测试页面;
  • 如果是,则容纳进设置和分拆步骤;
  • 渲染成HTML。
    那件事是什么?函数是做了一件事呢,还是做了三件事?注意,这三个步骤均在该函数名下的同一抽象层上。如果函数只是做了该函数名下同一抽象层上的步骤,则函数还是只做了一件事。编写函数毕竟是为了把大一些的概念(换言之,函数的名称)拆分为另一抽象层上的一系列步骤。
    代码清单1明显包括了处于多个不同抽象层级的步骤。显然,它所做的不止一件事。即便是代码清单2也有两个抽象层,这已被我们将其缩短的能力所证明。然而,很难再将代码清单3做有意义的缩短。可以将if语句拆出来做一个名为includeSetupAndTeardonwsIfTestpage的函数,但那只是重新诠释代码,并未改变抽象层级。所以,要判断函数是否不止做了一件事,还有一个方法,就是看是否能再拆出一个函数,该函数不仅只是单纯地重新诠释其实现。

3.3 每个函数一个抽象层级

自顶向下读代码:向下规则
我们想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能偱抽象层级向下阅读了。我把这叫做向下规则。
换一种说法。我们想要这样读程序:程序就像是一系列TO起头的段落,每一段都描述当前抽象层级,并引用位于下一抽象层级的后续TO起头段落。

  • To include the setups and teardowns, we include setups, then we include the test page content, and then we include the teardowns.(要容纳设置和分拆步骤,就先容纳设置步骤,然后纳入测试页面内容,再纳入分拆步骤。)
  • To include the setups, we include the suite setup if this is a suite, then we include the regular setup.(要容纳设置步骤,如果是套件,就纳入套件设置步骤,然后再纳入普通设置步骤。)
  • To include the suite setup, we search the parent hierarchy for the“SuiteSetUp”page and add an include statement with the path of that page.(要容纳套件设置步骤,先搜索“SuiteSetUp”页面的上级继承关系,再添加一个包括该页面路径的语句。)
  • To search the parent. . . (要搜索……)

写出只停留于一个抽象层级上的函数是保持函数短小、确保只做一件事的要诀。让代码读起来像是一系列自顶向下的TO起头段落是保持抽象层级协调一致的有效技巧。

3.4 switch 语句

写出短小的switch语句很难。即便是只有两种条件的switch语句也要比我想要的单个代码块或函数大得多。写出只做一件事的switch语句也很难。Switch天生要做N件事。不幸我们总无法避开switch语句,不过还是能够确保每个switch都埋藏在较低的抽象层级,而且永远不重复。当然,我们利用多态来实现这一点。

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);
    }
}

switch语句埋到抽象工厂[9]底下,不让任何人看到。该工厂使用switch语句为Employee的派生物创建适当的实体,而不同的函数,如calculatePayisPaydaydeliverPay等,则藉由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 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 SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

3.5 使用描述性的名称

把示例函数的名称从testableHtml改为SetupTeardownIncluder.render。这个名称好得多,因为它较好地描述了函数做的事。我也给每个私有方法取个同样具有描述性的名称,如isTestableincludeSetupAndTeardownPages。好名称的价值怎么好评都不为过。记住沃德原则:“如果每个例程都让你感到深合己意,那就是整洁代码。”要遵循这一原则,泰半工作都在于为只做一件事的小函数取个好名字。函数越短小、功能越集中,就越便于取个好名字。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。例如,includeSetupAndTeardownPagesincludeSetupPagesincludeSuiteSetupPageincludeSetupPage等。这些名称使用了类似的措辞,依序讲出一个故事。实际上,假使我只给你看上述函数序列,你就会自问:“includeTeardownPagesincludeSuiteTeardownPagesincludeTeardownPage又会如何?”这就是所谓“深合己意”了。

3.6 函数参数

  • 最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。

  • 从测试的角度看,参数甚至更叫人为难。想想看,要编写能确保参数的各种组合运行正常的测试用例,是多么困难的事。如果没有参数,就是小菜一碟。如果只有一个参数,也不太困难。有两个参数,问题就麻烦多了。如果参数多于两个,测试覆盖所有可能值的组合简直让人生畏。

  • 输出参数比输入参数还要难以理解。读函数时,我们惯于认为信息通过参数输入函数,通过返回值从函数中输出。我们不太期望信息通过参数输出。所以,输出参数往往让人苦思之后才恍然大悟。

  • 对于二元函数,也应该想办法将其转换为一元函数。

  • 如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
  • 从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参数被共同传递,就像上例中的x和y那样,往往就是该有自己名称的某个概念的一部分。
  • 如果可变参数像上例中那样被同等对待,就和类型为List的单个参数没什么两样。这样一来,String.formate实则是二元函数。下列String.format的声明也很明显是二元的:public String format(String format, Object... args)
  • 给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式。例如,write(name)就相当令人认同。不管这个“name”是什么,都要被“write”。更好的名称大概是writeField(name),它告诉我们,“name”是一个“field”。最后那个例子展示了函数名称的关键字(keyword)形式。使用这种形式,我们把参数的名称编码成了函数名。例如,assertEqual改成assertExpectedEqualsActual(expected, actual)可能会好些。这大大减轻了记忆参数顺序的负担。

3.7 无副作用

副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
普遍而言,应避免使用输出参数。如果函数必须要修改某种状态,就修改所属对象的状态吧。

public class 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函数,顾名思义,就是用来检查密码的。该名称并未暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。

3.8 分隔指令与询问

函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。

3.9 使用异常替代返回错误码

使用错误码会导致更深层次的嵌套结构。当返回错误码时,就是在要求调用者立刻处理错误。

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;
}

如果使用异常替代返回错误码,错误处理代码就能从主路径代码中分离出来,得到简化:

try {
    
    
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
    
    
    logger.log(e.getMessage());
}

3.10 抽离Try/Catch代码块

  • Try/catch代码块丑陋不堪。它们搞乱了代码结构,把错误处理与正常流程混为一谈。最好把try和catch代码块的主体部分抽离出来,另外形成函数。
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());
}

  • 函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。

3.11 别重复自己

因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
代码的重复会导致问题,因为代码因此而臃肿,且当算法改变时需要修改多处地方。而且也会增加多次放过错误的可能性。

3.12 写函数步骤

  • 写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。
  • 会配上一套单元测试,覆盖每行丑陋的代码。
  • 然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。
  • 最后,遵循本章列出的规则,我组装好这些函数。
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");
    }
}

猜你喜欢

转载自blog.csdn.net/qq_31654025/article/details/133202440