2012年1月5日 星期四

Clean Code - Functions

這章的內容是在教你怎麼寫出簡潔的function
以下是一個錯誤示範,你花三分鐘可能還看不懂他在寫什麽
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();
    }

以下是經過重構過得程式,是不是看起來容易懂多了
  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(); 
  } 
  • Small!

    • Function should be small
    • Function以20行為最佳
      • 之前的例子再進一步重構
      • public static String renderPageWithSetupsAndTeardowns(PageData pageData,
            boolean isSuite) throws Exception {
            if (isTestPage(pageData)) {
                includeSetupAndTeardownPages(pageData, isSuite);
            }
            return pageData.getHtml();
        }
        
      • if, else, while其中的block最好只有一行,而那一行是一個function call

  • Do One Thing

    • 第一個範例會讓人看不懂是因為他做太多事情了
    • Function should do one thing. They should do it well. They should do it only.
    • 當寫完function後,要再檢查看看是否還能把其中內容再拆成另一個function

  • One Level of Abstraction per Function

    • 函式中的statments都要再同一個抽象層級上
    • 第一個範例中的getHtml()就是屬於較高層級的概念,append則是低層級的概念
    • Reading code from top to bottom
      • 就像讀報紙一樣

  • Switch Statements

    • 我們很難讓switch精簡短小
    • 使用abstract factory和多型來處理switch

  • Use Descriptive Name

    • You know you are working on clean code when each routine you read turns out to be pretty much what you expected."
    • 不要害怕使用long name,SetupTeardownIncluder.render會比testableHtml要來的好

  • Function Arguments

    • 最理想的function是沒有參數,其次是一個參數,再來是兩個參數
    • 除非是特殊理由,否則不要使用三個以上的參數
    • 盡量避免使用output arguments,因為大多數人預期function由參數輸入並且由return回傳,output arguments會讓人想比較久
      • boolean fileExists("MyFile") 透過參數來問一個問題
      • InputStream fileOpen("MyFile") 操作一個參數並轉換成InputStream回傳
    • 還有一種形式是event,void passwordAttemptFailedNtimes(int attempts)
      • 此形式沒有return value,通常用於設定系統參數
      • 小心為他命名,務必讓讀者知道這是一個event
    • Flag Arguments
      • Flag arguments are ugly!
      • 違反Do One Thing的原則,盡量拆成兩個functions
    • 兩個參數的function
      • 比一個參數來的難懂
      • assertEquals(expected, actual),很容易搞混前後參數的次序,expected在前只是一種不成文的規定
      • 盡量拆解成一個參數的function,ex: writeField(outputStream, name)可改成outputStream.writeField(name)
    • 三個參數以上的function
      • 三個以上的參數非常難懂
      • 參數有三個以上,通常代表你需要把其中參數封裝成class,ex: x, y封裝成Point
      • 盡量拆解成一個參數的function,ex: writeField(outputStream, name)可改成outputStream.writeField(name)

  • Have No Side Effects

    • 執行以下這個function會有隱藏的side effect
    • 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;
        }
      }
      
    • 函式名稱並沒有提到會去call Session.initialize(),因此有一個隱藏的side effect
    • 一定要做的話要重新命名函式為checkPasswordAndInitializeSession (但是這違反Do One Thing)

  • Command Query Separation

    • 一個函式應該專門回答問題或是專心做某件事,而不是兩者都做
    • public boolean set(String attribute, String value),if (set("username", "unclebob"))...
      • set完參數後return是否成功,容易讓讀者搞混
    • 拆成兩個會比較好
    • if (attributeExist("username")) {
        setAttribute("username", "unclebob");
      }
      

  • Prefer Exceptions to Returning Error Codes

    • if (deletePage(page) == E_OK) 使用Exceptions來處理這種returning error codes

  • Don't Repeat Yourself

    • 不要在函式中複製貼上重複的code

  • Structured Programming

    • 在小函式中可以使用break和continue
    • 避免使用goto

    沒有留言:

    張貼留言