iT邦幫忙

2021 iThome 鐵人賽

DAY 8
0

https://ithelp.ithome.com.tw/upload/images/20210923/20138643Xzv7RnxUF1.png

「這個手環就像是為我的職業道德做出了公開聲明。它是一個明顯的指示,代表我承諾 『我將盡己所能把程式寫到最好』。所以它仍在我的手腕上,當我寫程式時,不斷提醒著我對自己曾經做出過,撰寫 Clean Code 的承諾」

取自: Clean Code (p.449)


  • 關於本書 (Clean Code, aka 無瑕的程式碼)
    目前為止,Clean Code 80% 以上的內容都已概括介紹到,其中:
    • CH11: 系統
      省略了後段的 Java Proxies 及 AOP 部分,有興趣的讀者可自行研究
    • CH13: 平行化
      超出筆者能力所及 Orz...,故略過
    • CH.14-16: 案例討論
      針對 Java 某些函式庫 (JUnit, SerialDate...) 進行重構的案例探討,算是選讀章節

CH17: 程式碼的氣味和啟發 (Code Smells & Inspiration)

  • 這一節是一套有關軟體紀律的價值體系統整 (Clean 學派?)
  • 有興趣深入了解的讀者可自行購買 Uncle Bob 另一本專書: Refactoring

註解 (Comments)

  • C1: 不適當的資訊 (Inappropriate Information)
    註解應該保留給技術性紀錄。某些像作者、修改紀錄、效能報告之類的詮釋資訊不該出現在註解中
  • C2: 廢棄的註解 (Obsolete Comment)
    當註解和原先描述的程式越來越遠時,代表註解過時了,盡快更新或移除它,避免不相關和誤導
  • C3: 多餘的註解 (Redundant Comment)
    如果程式碼已經拿表達意圖,那麼這段註解就是多餘的
  • C4: 寫得不好的註解(Poorly Written Comment)
    不要有廢話,要簡潔有力
  • C5: 被註解掉的程式碼 (Commented-Out Code)
    這是非常令人厭惡的事物,會令人抓狂!

開發環境 (Environment)

  • E1: 需要多個步驟以建立專案或系統 (Build Requires More Than One Step)
    你應該要能利用一個簡單的指令就可以取出整個程式系統,再下一個指令就能建好專案
  • E2: 需要多個步驟以進行測試 (Tests Require More Than One Step)
    同上,應該要一個指令就能快速簡單地執行所有的單元測試

函式 (Functions)

  • F1: 過多的參數 (Too Many Arguments)
    沒有參數是最棒的,其次是 1, 2, 3 個,盡量不要超過 3 個
  • F2: 輸出型參數 (Output Arguments)
    這是不直覺的,通常參數是輸入用的
  • F3: 旗標參數 (Flag Arguments)
    Boolean 參數違反了「只做一件事」原則,會造成困惑,應該移除
  • F4: 被遺棄的函式 (Dead Function)
    移除不再被呼叫的函式。你的版本控制系統會記得這些函式

一般狀況 (General)

  • G1: 同份原始檔存在多種語言 (Multiple Languages in One Source File)
    盡量讓同份原始檔使用的語言數量減少
  • G2: 明顯該有的行為未被實現 (Obvious Behaviour is Unimplemented)
    最少驚奇原則 (The Principle of Least Surprise):讀者要能從函式名稱直覺地瞭解函式本意
  • G3: 在邊界上的不正確行為 (Incorrect Behavior at the Boundaries)
    察看所有的邊界條件,並替它們寫測試
  • G4: 無視安全規範 (Overridden Safeties)
    不要忽視編譯器警告或未通過的測試
  • G5: 重複的程式碼 (Duplication)
    本書最重要的規範之一,Don't Repeat Yourself (DRY)
  • G6: 在錯誤抽象層次上的程式碼 (Code at Wrong Level of Abstraction)
    劃分「高層次一般概念」和「低層次細節概念」 (e.g., 所有的高層概念放在基底類別,低層概念放在衍生類別)
  • G7: 基底類別相依於其衍生類別 (Base Classes Depending on Their Derivatives)
    基底類別不應該知道任何有關衍生類別的資訊 (例外: 有限狀態機實作)。甚至可以再把基底和衍生類別拆到不同檔案裡
  • G8: 過多的資訊 (Too Much Information)
    優質的軟體開發者會限制類別和模組中暴露的介面數量。類別擁有的方法和實體變數愈少愈好、函式知道的變數愈少愈好
  • G9: 被遺棄的程式碼 (Dead Code)
    請移除不會被執行的程式碼
  • G10: 垂直分隔 (Vertical Separation)
    變數和函式定義應該要很靠近被使用的地方
  • G11: 不一致性 (Inconsistency)
    遵守慣例 (例: 維持變數命名的一致性)
  • G12: 雜亂的程式 (Clutter)
    沒有任何實作的預設建構子、不被使用的變數、函式、沒有資訊的註解...等,都該被移除
  • G13: 人為的耦合 (Artificial Coupling)
    不要讓「沒有直接目的」的兩個模組耦合、不要將變數、常數或函式放置在一個臨時方便但不洽當的地方
  • G14: 特色留戀 (Feature Envy)
    類別的方法應該只對同一類別裡的變數和函式感興趣,而不是留戀其他類別裡的資訊 (e.g., 使用「別的物件」的 setter 和 getter)。這是強烈程式碼氣味之一,
  • G15: 選擇型參數 (Selector Arguments)
    沒有什麼比函式呼叫掛上一個 True / False 參數要來的令人厭惡 (e.g., calculateWeeklyPay(false))
  • G16: 模糊的意圖 (Obscured Intent)
    程式應盡可能地具有表達力,避免魔術數字 (Magic Number)、跨行的表達式、匈牙利命名法...等
    // (X) 程式碼不是愈短愈好!
    public int m_otCalc() {
      return iThsWkd * iThsRte +
        (int) Math.round(0.5 * iThsRte *
          Math.max(0, iThsWkd - 400)
      );
    }
    
  • G17: 錯置的職責 (Misplaced Responsibility)
    程式碼應該被放在一個讀者自然而然會認為它該存在的地方
  • G18: 不適當的靜態宣告 (Inappropriate Static)
    少用靜態方法。如果真的想要一個靜態函式,先確認你不可能想讓函式有多型行為
  • G19: 使用具解釋性的變數 (Use Explanatory Variables)
    讓程式可讀性提升的最有效方式之一,是將計算過程拆解成許多富有意義名稱的暫存變數
  • G20: 函式名稱要說到做到 (Function Names Should Say What They Do)
    如果你必須看到函式的實現內容或文件,才能瞭解這個函式在做什麼,那你應該替函式換一個更好的名稱
    Date newDate = date.add(5);
    
    // vs.
    
    Date daysLater = date.addDaysTo(5);
    
  • G21: 瞭解演算法 (Understand the Algorithm)
    只是想辦法讓函式通過所有的測試還不夠,必須知道解決方案為何是正確的
  • G22: 讓邏輯相依變成實體相依 (Make Logical Dependencies Physical)
    模組不應該對被相依的模組有任何預先的假設 (邏輯相依),它僅向被相依的模組詢問所需的訊息
  • G23: 用多型取代 If/Else 或 Switch/Case (Prefer Polymorphism to If/Else or Switch/Case)
    One Switch 原則: 對於給定的選擇型態,不應有超過 1 個以上的 switch 敘述
  • G24: 遵循標準的慣例 (Follow Standard Conventions)
    團隊要有一套程式碼開發標準規範
  • G25: 用有名稱的常數取代魔術數字 (Replace Magic Numbers with Named Constants)
    這是最古老的規範之一,COBOL、FORTRAN 的手冊都如此建議。除非是一些能自我解釋或很容易被辨識的常數才可省略
    // (X)
    if(page.size() == 55){
      //...
    }
    
    // (O)
    if(page.size() == page.MAX_PAGE_SIZE){
      //...
    }
    
    int dailyPay = hourlyRate * 8;
    double circumference = radius * PI * 2;
    
  • G26: 要精確 (Be Precise)
    e.g., 不要用 Float 來表示貨幣、能只用 List 就不要宣告成 ArrayList、如果函式可能回傳 NULL,就要有檢查機制、如果可能出現同步問題、就要確保有使用鎖定 (Lock) 機制
  • G27: 結構勝於常規 (Structure over Convention)
    「具強制決策設計特性」的結構勝過「慣例」
  • G28: 封裝條件判斷 (Encapsulate Conditionals)
    // (O)
    bool shouldBeDeleted = timer.hasExpired() && timer.isRecurrent() == false;
    if(shouldBeDeleted) {
      ...
    }
    
    // (X)
    if(timer.hasExpired() && timer.isRecurrent() == false) {
      ...
    }
    
  • G29: 避免否定的條件判斷 (Avoid Negative Conditions)
    if(shouldBeDeleted) {...}
    
    // vs.
    
    if(!shouldNotBeDeleted) {...}
    
  • G30: 函式應該只做一件事 (Functions Should Do One Thing)
    public void paySalary() {
      foreach(Employee e in employees) {
        if(e.isPayDay()) {
          Money pay = e.calculatePay();
          e.deliverPay(pay);
        }
      }
    }
    
    上述函式做了三件事:檢視所有職員、檢查哪幾位職員該獲得薪資、付薪資。應當改寫成
    public void paySalary() {
      foreach(Employee e in employees){
        payIfNecessary(e);
      }
    }
    
    private void payIfNecessary(Employee e) {
      if(e.isPayDay()) {
        calculateAndDeliverPay(e);
      }
    }
    
    private void calculateAndDeliverPay(Employee e) {
      Money pay = e.calculatePay();
      e.deliverPay(pay);
    }
    
  • G31: 隱藏時序耦合 (Hidden Temporal Couplings)
    當函式的呼叫須依照次序時 (e.g., 先...、再...、才能...),不應該隱藏這些耦合。建立生產線,讓每個函式都產生下一個函式需要的結果 (防止呼叫次序被誤改)
  • G32: 不要隨意 (Don't Be Arbitrary)
    組織程式碼應該要有理由,而且要確保理由與程式碼結構有關
  • G33: 封裝邊界條件 (Encapsulate Boundary Conditions)
    將邊界條件的處理放置於同一個地方
  • G34: 函式內容只該下降一個抽象層 (Functions Should Descend Only One Level of Abstraction)
    函式裡的敘述都要在同樣的抽象層次上,這些敘述只能比函式名稱低一個抽象層次。劃分抽象層次概念,是進行函式重構時,最重要也最難做到的事情之一
  • G35: 可調整的資料應放置於高階層次 (Keep Configurable Data at High Levels)
    將設定性質的常數放在非常高階的位置、並向下傳遞到程式的各個角落
    public static void main(String[] args) { 
      Arguments arguments = parseCLI(args);
      ...
    }
    
  • G36: 避免傳遞性導覽 (Avoid Transitive Navigation)
    確保模組只須了解它們的立即合作者,並不需要了解整個系統的導覽圖
    a.getB().getC().doSomething();
    
    以上例來說,將來若想在模組 B 和 C 之間安插新的模組 Q 時,必須找出所有的 "a.getB().getC()" 敘述,並安插入 getQ(),這使架構變得生硬,我們不應漫步在系統的物件圖裡,倒不如讓立即的合作者提供所需要的服務
    myCollaborator.doSomething();
    

JAVA

  • J1: 利用萬用字元來避免冗長的引入列表 (Avoid Long Import Lists by Using Wildcards)
    宜使用 import package.*; (但要注意,少數情況會遇到名稱衝突)
  • J2: 不要繼承常數
    不要將常數放在介面裡,然後透過繼承介面取得使用權。盡量用靜態 (Static) 的引入敘述來取代
  • J3: 常數 vs. 列舉 (Constants versus Enums)
    多善用 enum,不要用 public static final int 這種古老技巧

命名 (Names)

  • N1: 選擇具描述性質的名稱 (Choose Descriptive Names)
    命名因素在程式可讀性上所佔的比率達九成以上。好的名稱描述了程式的結構
  • N2: 在適當的抽象層次選擇適當的命名 (Choose Names at the Appropriate Level of Abstraction)
    選擇能反映出類別或函式抽象層次的名稱,不要把不同的層次混在一起
  • N3: 盡可能使用標準命名法 (uSE sTANDARD nOMENCLATURE wHERE pOSSIBLE)
    使用大家熟知的慣例或用法來進行命名 (e.g., ModemDecorator),這樣的行為也叫做專案裡的「普及語言 (Ubiquitous Language)」
  • N4: 非模稜兩可的名稱 (Unambiguous Names)
    選擇不會讓函式或變數意義模稜兩可的名稱,可使用長命名 (解釋性價值更高)
  • N5: 較大範圍的視野使用較長的名稱 (Use Long Names for Long Scopes)
    變數或函式的視野範圍小,可使用較短的命名,反之亦然
  • N6: 避免編碼 (Avoid Encodings)
    現今的開發環境已經不必將型態或視野編碼到名稱裡了 (e.g., 不要用匈牙利命名法)
  • N7: 命名應該描述可能的程式副作用 (Names Should Describe Side-Effects)
    名稱應該描述所有事情,要從命名看得出程式的副作用 (e.g., getOos() vs. createOrReturnOos())

測試 (Tests)

  • T1: 不足夠的測試 (Insufficient Tests)
    測試組應該包含所有可能失敗的情況
  • T2: 使用涵蓋率工具 (Use a Coverage Tool)
    工具會告訴你,那些地方並未被測試到
  • T3: 不要跳過簡單的測試 (Don't Skip Trivial Tests)
    簡單的測試很容易撰寫,且測試文件是有價值的
  • T4: 被忽略的測試是對模稜兩可的疑問 (An Ignored Test Is a Question about an Ambiguity)
    因為程式的需求不夠明確,所以無法確定某個行為的細節
  • T5: 測試邊界條件 (Test Boundary Conditions)
    特別注意測試邊界條件
  • T6: 在程式錯誤附近進行詳盡的測試 (Exhaustively Test Near Bugs)
    程式 Bugs 往往會聚集,當找到某個程式錯誤,最好對該函式做詳盡的測試
  • T7: 失敗的模式是某種啟示 (Patterns of Failure Are Revealing)
    利用測試程式失敗的模式來診斷程式問題 (e.g., 所有比 5 個字元長的測試都失敗了?)
  • T8: 測試涵蓋率模式可以是一種啟示 (Test Coverage Patterns Can Be Revealing)
    查看在測試時執行過或未被執行過的程式碼,有助了解測試為什麼失敗
  • T9: 測試要夠快速 (Tests Should Be Fast)
    當行程很趕時,緩慢的測試會被移除,所以要保持測試夠快

感謝所有追蹤到這邊的各位讀者們,您的支持與訂閱是我創作的最大動力。希望各位開發者們都能夠在未來持續發揮專業精神,撰寫 Clean Code (無瑕的程式碼),如同 Martin Flower 所承諾:「我將盡己所能把程式寫到最好」


上一篇
Day 07: 類別、系統、羽化
下一篇
Day 09: 【番外篇】關於寫 Code 這件事 (待改進中... )
系列文
成為乾淨的開發者吧! Clean Code, Clean Coder, Clean Architecture 導讀之旅31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言