1. 程式人生 > 實用技巧 >Clean Code讀書筆記(2)---函式

Clean Code讀書筆記(2)---函式

1. 短小

函式應該儘可能短小。每個函式20行封頂最佳。

2. 只做一件事(單一職責)

每個函式應該只做一件事,這樣也可以保證函式儘可能短小。函式名字可以對程式碼進行自我描述。這樣可讀性會非常好。

3. 每個函式只做一個抽象層級的事

對於第二點 “一件事”的定義,每個人可能也有不同的理解。我們必須利用另一個概念來定義“一件事”。 抽象層級。函式類每行程式碼都必須在同一抽象層級上,這樣函式就算是隻做了一件事。那抽象層級又是什麼呢?

可以將程式定義成一系列的To起頭的段落,每一個段落都描述當前抽象層級,並引用下一抽象層級。例如:

To include the setup and teardown, we include setups, then we include the test page content, and then include the teardowns. (這裡面include setup, include test page content, include teardowns是同一抽象層級,可以通過定義新的函式來隔離他們的複雜度。)

To include the setups, we include the suite setup if this is a suite, then we include the regular setup(這裡定義了include the setup這一個抽象層級,這裡面的suite setup, regular setup是同一抽象層級,)

To include the suite setup, we search the parent hierarchy and add an include statement with the path of that page(這裡定義了include the suite setup這一個抽象層級,search, add an include statement屬於同一抽象層級)

分析一下如下程式碼,加深一下理解:

package com.zboot.cleancode.chapter03;

public class HtmlUtil {

    public static String testableHtml(PageData pageData, boolean isSuiteSetup) throws Exception {

        boolean isTestPage = pageData.hasAttribute("Test");
        if (isTestPage) {
            WikiPage testPage = pageData.getwikiPage();
            StringBuffer newPageContent 
= new StringBuffer(); includeSetupPages(testPage, newPageContent, isSuiteSetup); newPageContent.append(pageData.getContent()); includTearDownPage(testPage, newPageContent, isSuiteSetup); pageData.setContent(newPageContent.toString()); } return pageData.getHtml(); }
   // 上面這個函式其實包含了多個抽象層級,在同一抽象層級的應該是
1. if has "test" attribute, include setup and tear down page(至於如何include setup and tear down,應該是下一抽象層級的事)
2. generate html
private static void includeSetupPages(WikiPage testPage, StringBuffer newPageContent, boolean isSuiteSetup) {

        WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", testPage);
        if (setup != null) {
            WikiPagePath setupPath = testPage.getPageCrawler().getFullPath(setup);
            String setupPathName = PathParser.render(setupPath);
            newPageContent.append("!include -setup.").append(setupPathName).append("\n");
        }

        if (isSuiteSetup) {
            WikiPage suiteSetup = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, testPage);
            if (suiteSetup != null) {
                WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullpath(suiteSetup);
                String pagePathName = PathParser.render(pagePath);
                newPageContent.append("!include -setup.").append(pagePathName).append("\n");
            }
        }

    }
// 上面這個函式也包含了多個抽象層級,處於同一抽象層級的應該是
1. include setup information(至於具體如何inlcude,是下一抽象層級的事)
2. if should include suite setup, include suite setup information
private static void includTearDownPage(WikiPage testPage, StringBuffer newPageContent, boolean isSuiteSetup) { WikiPage teardown = PageCrawlerImpl.getInheritedPage("TearDown", testPage); if (teardown != null) { WikiPagePath tearDownPath = testPage.getPageCrawler().getFullPath(teardown); String tearDownPathName = PathParser.render(tearDownPath); newPageContent.append("\n").append("!include -teardown.").append(tearDownPathName).append("\n"); } if (isSuiteSetup) { WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, testPage); if (suiteTeardown != null) { WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath(suiteTeardown); String pagePathName = PathParser.render(pagePath); newPageContent.append("!include -teardown.").append(pagePathName).append("\n"); } } } }
// 上面這個函式也包含了多個抽象層級,處於同一抽象層級的應該是
1. include teardown information(至於具體如何inlcude,是下一抽象層級的事)
2. if should include suite setup, include suite teardown information

經過分析,其抽象層次結構如下:

然後程式碼按照這個思路可以重構成如下這樣(由於多個函式需要共享一些引數,比如重新生成頁面內容的StringBuilder,所以將整個過程封裝為型別):

package com.zboot.cleancode.chapter03;

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 = testPage.getPageCrawler();
        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() {
        includeSetupPages();
        includePageContent();
        includeTearPages();
    }

    private void includeSetupPages() {
        if (this.isSuiteSetup) {
            include("SuiteResponder.SUITE_SETUP_NAME", "-setup");
        }
        includeSetupPage();
    }

    private void includeSetupPage() {
        include("SetUp", "-setup");
    }

    private void includePageContent() {
        newPageContent.append(pageData.getContent());
    }

    private void includeTearPages() {
        if (this.isSuiteSetup) {
            include("SuiteResponder.SUITE_SETUP_NAME", "-teardown");
        }
        includeTearPage();
    }

    private void includeTearPage() {
        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);
            newPageContent.append("!include -").append(args).append(".").append(pagePathName).append("\n");
        }
    }

}

4.使用描述性的名稱

函式功能越單一,越短小,就越便於取一個好的名字。函式名稱要有自我描述的功能。讀者看到函式名變知道函式的用途是什麼。命名方式要保持一致。使用同一的短語,名詞和動詞給函式命名。

5. 函式引數

函式引數儘量保持在3個及3個以下。如果超過3個最好將其中一些引數封裝成物件。

6. 無副作用

函式名用來描述函式的用途,如果再函式內部做了一些函式名描述不了的事情。則產生了副作用,我們應該儘量要避免函式有副作用。舉例:

public boolean checkPassword(String username, String password) {
  User user = UserGateway.findByName(userName);
  if(user != null) {
    if(user.Password == password) {
       Session.Initialize(); //這裡產生了副作用。呼叫者如果不看程式碼的話有可能錯誤地呼叫該方法。
       return true;
    }
  }
  
  return false;  
}

7.分隔指令和詢問 (讀寫應該分離)

不好的例子:public boolean set(string attribute, string value)來實行寫入資料到某個attribute.這裡如果要寫入資料,就不要有boolean型別的返回值。

8. 使用異常替代返回錯誤碼

9. 抽離Try/Catch 塊

把Try Catch邏輯剝離出來單獨作為一個函式,遵循單一職責原則。例如:

public void delete(Page page) {
   try {
       deletePageAndAllReference(page);
   }
   catch(Exception e) {
       logError(e)
   }
}

private void deletePageAndAllReference(Page page){
//.....
}

private void logError(Exception e) {
//....
}

10. 遵守DRY原則

Don't Repeat Yourself,一旦你發現有幾處程式碼重複,說明你需要進行重構。抽取公共方法了。