Clean Code: Function 函式

Clean Code: Function 函式

Chapter 3: Function

  • 簡短!

    關於函式的首要準則就是簡短,第二項準則就是要比第一項的簡短函式還要更簡短。

  • 區塊(Blocks)和縮排(Indenting)

    If、else、while及其他描述都應該只有一行,而那行通常是函式呼叫描述。不僅能維持封閉函式的簡短,區塊內呼叫的函式名稱也能來描述意圖。

    public static String renderPageWithSetupsAndTeardowns(PageData pageData, bool isSuite) {
        if (isTestPage(pageData))
           includesSetupAndTeardownPages(pageData, isSuite);
        return pageData.getHtml();
    }
    
  • 只做一件事情

    函式應該只做一件事情

  • 函式的段落

    如果一個函式被拆成幾個段落,包含宣告區 declarations、初始區 initializations、 過濾區 sieve,這是做超過一件事情的徵兆

  • 每個函式只有一層抽象概念

  • 由上而下閱讀程式碼:降層準則

    我們希望在閱讀程式碼時,能夠像閱讀一連串TO段落,每個段落描述著目前所處的抽象層次,並且提及了接續的下個層次的TO段落

  • Switch描述

    Switch函式問題一可能會太冗長,問題二涵式可能做超過一件事情,問題三可能違反單一職責原則(SRP),第四點當新型態加入後,函式必須改變所以它違反了開放封閉原則(OCP)

    作者認為可以使用switch描述來產生Emplyee介面(interface)的實體(instance),另外,其他不同的函式都藉由介面,透過多型的方式來指派,例如calculatePay(計算薪水)、isPayDate(是否為發薪日)、deliverPay(發薪水)

  • 使用具描述能力的名稱

    當每個你看到的程式,執行結果都跟你想得差不多,你會察覺到你正工作在Clean Code上

    維持命名的一致性,在你的模組裡,使用一致的片語、名詞和動詞來替函式取名

  • 函式的參數

    最理想是零參數函式,其次是一個,盡量避免使用三個參數。

  • 單一參數的常見形式

    1.與這個參數有關的問題,例如 boolean fileExists("MyFile")

    2.對參數進行某種操作,例如 InputSteam fileOpen("MyFile")

    你應該選擇能明顯區分這兩種的名稱,如果一個函式會轉換輸入的參數,則轉換後的產物應該出現在回傳值裡

  • 兩個參數的函式

    writeField(name) 比 writeField(outputStream, name) 更容易理解
    outputStream.writeField(name) 是更好的方式
    
  • 三個參數的函式

    使用者需反覆閱讀查看才能理解

  • 物件型態的參數

    Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point center, double radius);
    

    可以使用類別來統整概念相似的參數

  • 動詞和關鍵字

    函式和參數要形成一個動詞/名詞的良好配對,例如寫入名稱write(name),name會被寫入,或writeField(name)更能告訴我們name是一個欄位(field)。

    關鍵字式的命名,例如 assertEquals -> assertExpectedEqualsActual(expected, actual),減輕了需記住參數的負擔

  • 要無副作用(side effects)

    你的函式保證只做一件事,但卻暗地裡偷偷做了其他事情

    public class UserValidator {
        public boolean checkPassword(string userName, string password) {
    	  User user = UserRep.findByName(userName);
          if (user != null) {
              if (user.password == password) {
                  Session.initialize(); 
                  //side effects 使用者並不知道此函式內會改變Session,這是有風險的                                     //,除非將函式名稱修改為checkPasswordAndInitializeSession
                  return true;
              } else {
                  return false;
              }
          }
        }
    }
    
  • 輸出型的參數

    要避免使用輸出型參數,如果函式必須要改變物件的某種狀態,就讓該物件改變其本身的狀態吧

    public void appendFooter(StringBuffer report); 
    -> report.appendFooter();
    
  • 指令和查詢的分離

    將指令Command和查詢Query分開,避免模稜兩可的情形。

    public boolean set(String attribute, String value);
    if (set("username","unclebob")) ... 
    //無法判斷是詢問username被設為unclebob或是將username設為unclebob並回傳...
    //應該修改為    
    if (attributeExists("username")) {
        setAttribute("username","unclebob");
    }    
    
  • 使用例外處理取代傳回錯誤碼

    //將指令函式當作判斷表達式用
    if (deletePage(page) == E_OK)
    //可能回導致更深層的巢狀結構
    if (deletePage(page) == E_OK) {
        if (registry.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());
    }
    
  • 結構化程式設計

    每個函式裡的區塊,都應該只有一個進入點跟一個離開點,迴圈內不能有任何的break或continue描述,且永遠不可以有goto描述

    將函式分開、重新命名、減少重複、縮短方法並重新安排方法的順序,有時候打散整個類別,並持續保持單元測是可以通過

  • 不要重複自己

    重複程式碼也許是軟體裡所有的邪惡根源,物件導向就是一種減少重複程式碼的策略