Try   HackMD

Clean Code 乾淨的程式碼

命名

  • 取有意義且精準表達的名字,不帶有歧意
  • 是處理用途的方法,就用動詞表達(如postForm());
    變量盡量表達及意(如userAccountName);
    類名(class)則以名詞短語為主(如Customer, User)
  • 不要使用魔術數(使用像是DEFAULT_WIDTH,而非直接使用100)

函數

  • 無副作用
  • 短小,只做一件事
  • 一元最好,二元次之,三元更次之,三元以上建議包裝為物件
  • 使用標示性的名稱
  • 每個函數一個抽象層級
  • 錯誤處理

註解

總之不要寫就對了 :)
讓函數或變數本身就可以說明自己

格式

縮排
對齊

數據與結構

對象曝露行為,隱藏數據
數據暴露結構,沒有明顯的行為

錯誤處理

將錯誤處理分開隔離,獨立於主邏輯之外,就可以單獨處理其錯誤

邊界

API邊界
尚未存在的程式邊界

單元測試

可讀性!!
Build-Operate-Check 構造-操作-檢驗
測試宣告:BDD(Behavior Driven Development): Given-When-Then原則

  • 不該有太多介面相關細節,否則UI一旦更改則全部可能都要更改
    每個測試一個概念
  • 像是如果測試某函數有兩種案例行為,則該把它分成兩個測試 >> 因為偷懶常常放在一起測試
    F.I.R.S.T
    Fast:快速 頻繁且快速地進行測試
    Independent:獨立 每個測試互相獨立且不受順序影響
    Repeatable:可重複 無論在任何環境皆可以重複測試成功
    Self-Validating:自我驗證 測試本身應該要有boolean輸出以自動檢驗測試,不該用log檔手動看
    Timely:及時 要在實際運作的code之前編寫,否則code可能會被發現難以測試

class 類

應該短小,且更加短小

  • 單一權責原則(Single Responsibility Principle):
    class / module應該只會有一條修改其的理由,一個系統應該由許多短小的class而不是少量龐大的class組成,每個小class只封裝一個權責,且只有一個修改他的理由,與其他class達成期望的系統行為

  • 內聚性
    class 中應該只有少數的變量,class內的方法應該要操作一或多個其中的變量,這表示class的方法與變數互相有關,形成一個邏輯整體

  • OCP Open-Close-Principle 開放封閉原則
    透過將class中各個方法拆解為class的派生(繼承)子類別後,可以利用擴充方式來擴充既有的子類別,而非動到原始的class去更改程式碼
    e.g. 像是class apple 有許多方法(make flowers, get taller, add one leaf等),透過將其各種方法拆解成class bloom extends apple, class grow extends apple,這樣我們就可以利用派生的子class去擴充子class的方法,而不會動到原本的class apple

系統

測試驅動系統架構,而非先做大設計(從整個系統框架都想好後再做),在之後的迭代和更新新增功能會更加彈性
整潔的系統有利於敏捷開發
使用大概可以工作的最簡單方案

迭代

簡單設計的四條規則(重要性由上至下)

  1. 跑過所有測試
    可以測試即為可以驗證,才能部署
    只要可測試,就會趨向短小單一的設計方案

2~4:重構
每次增加或修改代碼之後,回顧其修改後的代碼,讓設計更加整潔(提升內聚性 降低耦合 模塊化 縮小尺寸 更好的命名),並運行測試且通過它!

  1. 不可重複
    把可以共用的部分抽取出來,如此一來不僅減少測試,且也減少額外的風險(萬一被修改或刪除)

  2. 表達程式設計者的意圖
    如果花十分鐘寫程式(一般較具有系統的專案),讀9分寫1分,不管是新增還是之後的維護,都會花大量的時間去理解既有的程式碼,因此如果能把程式保持短小精簡,且命名各個功能/變數到位,那麼讀起來會更加輕鬆,你開心大家都開心 :)

  3. 盡可能減少classes / methods的數量
    不要有太多細小的classes / methods,但是此條規則為優先程度最低的,意即在達到以上三點之後,再來考慮這點吧!

開發編碼

開發上的防禦原則

  1. SRP 單一權責原則
    將多線程執行的代碼和其他代碼分離,否則如果商業邏輯與多線程互相耦合,則會難以修改與除錯

  2. 限制數據作用域
    當數據有共享時,要注意各個線程可能會互相干擾,導致無法預期的錯誤,因此要限制其數據的作用區域範圍!

  3. 使用數據複製
    呈上,避免共享數據是更好的方法,透過複製唯讀數據的方式來拿取數據的副本,在最後再將其數據做整合處理

  4. 多線程之間盡可能獨立
    各個線程就像是平行世界,從源頭複製所需的數據,再各自處理和暫存數據,並可以再透過資料庫的方式共享資源

逐步改進

一開始就寫完美程式,是不可能的!擁抱醜陋,先從醜醜的草稿開始吧!

再來逐漸改進,越改越好,但不是說一開始能工作的醜醜程式就把她留著(很多人寫完能動的程式碼就認為完工了),一有機會就要將他重構或改寫的更好!

透過一次次的小小重構和抽離,並通過測試確保程式可行,將程式各個模組分開,降低依賴耦合程度!

如果在上午寫了混亂的程式碼,下午就趁熱把它解決,不讓混亂變得更亂!

重構

  • 透過抽象化方法,將程式寫得更加簡潔且清楚,在每次寫程式後都比原來更整潔

  • 將各個方法或函式分類整理到適合該class或檔案的地方,讓語意更加達意

  • 修改變數名稱,讓變數名稱能更符合修改後的情境,且更好理解

味道與啟發

  • 註釋
    冗餘或程式本身就可以解釋的註釋都是不用寫的!
    特別是被注釋掉的程式,現在可以透過git等工具去取得之前的版本,儘管刪掉沒關係!

  • 函數

    1. 過多的參數: 沒參數最好,一個次之,二個 三個再次之,三個以上最好包成物件或是拆分,盡量避免
    2. 標示參數:像是透過true / false 來做兩件事的參數,就把它拆成兩個吧
    3. 不要用的函數:一樣也是把它刪掉,反正git都會記得!
  • 一般問題

    1. 多語言問題:盡量減少文件中的多種語言(以一種為主)
    2. 沒實現明顯的行為:明顯的行為應該要被實現,像是如果有個函數(monthNumToMonthStr)是將月份(數字: 1, 2, 3)轉換成月份(字串: Jan),那麼就應該要有期望的輸出,否則就要看源代碼的細節了
    3. 不正確的邊界行為:代碼應該要有辦法在大多數的邊界與情況下運作
    4. 忽視安全:例如把某些編譯的警告關掉,或關掉失敗測試,這有可能導致之後的除錯與維護災難
    5. 重複:維持DRY(Don't Repeat Yourself),將重複的代碼提高層次(抽象化),如此一來不僅可以減少測試,也可以減少錯誤發生的機會(例如複製貼上遺漏了)
    6. 錯誤的抽象層級:越通用與抽象的層級,通常放在基礎類別;而越具體及細節相關的層級,會放在派生的類別,例如Radius 這個屬性應該放在Circle之下,而非Shape
    7. 訊息過多:一個class / module提供過多的接口,將會造成太深太廣的耦合,導致難以修改或是未知的問題,應該要限制其能使用的方法/ 接口/內部變數
    8. 死碼:不會執行到的代碼,透過方法得知該方法不會被執行時,就果斷刪掉吧!
    9. 選擇算子參數:透過輸入的參數來決定要做函數中的哪一件事,例如透過true/false來決定要對calc(isPluse)中做加或減,這樣就必須記住該函數的詳細規則,且每次調用都要去查看!
    10. 晦澀的意圖:代碼本身就要具有表達力,任何無法一眼就大概知道意圖的(特別是魔術數),像是time = prevTime + 1000,此時的1000就不知道為何,要改成 time = prevTime + thounsandMillSec相對比較好
    11. 權責位置錯誤:代碼應該在讀者覺得他應該在的地方,像是getSalary就應該在Employee之類的類別底下
    12. 用命名常數來替代魔術數:像是38, 可以用Keycode.ARROW_UP之類的來替代,來更表達其數字含義
    13. 函數應該要只做一件事:函數一次應該只做一件事,像是判斷if/else, for,
    14. 隱藏了時序耦合:如果代碼的執行順序有其順序性,那麼可以透過return值的方式,傳遞處理好的結果給下一個函數使用,如此一來就清楚個函數的順序了
    15. 封裝邊界條件:例如lastIndex = array.length - 1 之類的,應該要抽離作為變數集中在一起,會更加清楚!
    16. 拆分不同抽象層級:像是 code.16
    17. 將可以被配置的值放在較高的抽象層級,不要埋到底下的派生或較低層級,不然就要一個個去找
    18. 避免傳遞瀏覽:避免像是 A.getB().getC().getD()之類的語法,盡量將方法直接曝露出來,像是 A.getDeepD() 之類的d

code.16

//此範例就是混合了兩個抽象層級, 一個是針對字串的直接操作,一個是調用函數 function renderTypedString(str: string) { return '_' + str + getSeparatedStr(str) } //可以改成像是以下,不僅比較好測試,且更清楚! function getUnderlinedStr(str: string)) { return '_' + str } function renderTypedString(str: string) { return getUnderlinedStr(str) + getSeparatedStr(str) }
  • 名稱
    1. 採用描述性的名稱:除非像是for中的i這種常用名稱,否則都應該要取名得一眼就能看出他是什麼用途的名稱,像是
      newScore = score + averageScore 而非
      k = j + 10
    2. 使用具體且無其他意思的名稱:像是有個方法會在取得資料後,關閉資料庫的連接
      function doGetDataFromDB() { ... } 然而以上方法只有提到取得資料而沒有描述該關閉連接的功能,就應該改為
      function doGetDataFromDBThenDiconnectDB() { ... } 會比較好(儘管名稱真的很長)
  • 測試
    1. 測試不足:測試應該要測試所有可能失敗的狀況或是例外規則,只要有條件沒有被測到,就還不夠!
    2. 使用覆蓋率工具:前端React為例,你可以使用jest coverage來幫助你測試代碼的覆蓋率(但是不要完全相信,因為此覆蓋率僅為代碼有跑過那個部分,並不一定是有根據邏輯去測試過!)
    3. 別略過小測試:小測試不僅容易編寫,因為其表達與相對大測試,能更為快速驗證,因此更有價值去寫測試
    4. 測試邊界條件:注意測試邊界的條件,有時候會有邊界判斷錯誤的問題
    5. 測試失敗或許能夠沒發現過的問題:當在重構或是在測試完整用例時的測試錯誤,或許可以發現bug的解決方法!
    6. 測試應該快速:當時間不夠時(通常都是這樣),不會去運行比較慢的測試,因此盡量將測試精簡快速吧!

constants 用config等等的從外部引入