Ch2 複雜度(Complexity)| 可測試的 JavaScript (Testable JavaScript) 閱讀筆記
27 Mar 2021人為的「無法想清楚邏輯」或「複雜的演算法」往往導致寫出複雜的程式碼。因此解法即是透過辨別複雜的成因,來試著降低複雜度。
程式碼大小 (Code Size)
這裡指的「程式碼大小」是每個函式的大小,並非一個檔案的總行數或總字數,當然檔案內所包含的函式也應該盡量相關、少量。
為了精簡每個函式的大小,可用「命令查詢分離」來拆分程式碼,意即將函式拆分為命令 (command) 與查詢 (query)。
- 查詢:有回傳值,例如:傳回某個運算結果。
- 命令:做某事,無回傳值,例如:驗證傳入值是否符合規格。
這樣的拆分方式除了程式的精簡、可讀性外,也能讓測試方向更為明確、聚焦、有彈性。
備註
在這裡書中提到幾個觀念…
- 該防呆的要防呆,避免人為錯誤。
- 注意範疇,該是私密的就要保持私密,需要測試的就保持公開,可避免混淆;
- 利用
try...catch
拋出錯誤,並且錯誤應清楚明瞭。
JSLint
- JSLint 是用來量測程式碼可讀性與穩健性的工具,它能解析程式碼,找出淺在的問題 - 風格 (style)、語法 (syntax) 與語意 (semantics),因此有助於降機程式碼的複雜度與減少 bug。
- JSLint 或 JSHint 會提示開發者可能的問題所在,改進方法是儘量使用該程式語言內建或慣例,這樣就能順利產出易懂、好測試的程式碼。
JSLint 是較早期的工具,目前業界較常使用 ESLint。
循環複雜度 (Cyclomatic Complexity)
循環複雜度是指測量程式碼中,無相依關聯 (independent path) 內容的數量,意即計算在所有程式碼中,所需撰寫最少單元測試的數量,等同於程式碼覆蓋率 (code coverage) 必須達成 100% 的單元測試數量。
如何計算循環複雜度
工具
- jsmeter
- jscheckstyle + jenkins
- VSCode - CodeMetrics
在 VSCode 安裝 CodeMetrics,即可看到每個 function 的複雜度。
注意
- 一個函式的循環複雜度必須 <= 10,因為 10 表示 bad fix 約 5%, >16 則可靠度很低。
- 降低循環複雜度的解法是把函式切成更小的函式。
- 重構前一定要先寫測試,以確保程式碼的正確性。
範例
將 if/then/else
判斷改寫為查找表 (lookup table),雖然不減少必要撰寫 unit test 的數量,但可維護性提高。
修改前
針對單一函式,為多個分支而寫多個單元測試。
const doSomething = (a) => {
if (a === 'x') {
doX();
} else if (a === 'y') {
doY();
} else {
doZ();
}
};
單元測試範例。
describe('doSomething', () => {
it('a as x....');
it('a as y....');
it('a is not x nor y....');
});
修改後
針對多個比較小的函式,撰寫單一的單元測試,清楚明瞭即是好維護。
const doAnotherThing = (a) => {
const lookup = {
x: doX,
y: doY,
};
const def = doZ;
lookup[a] ? lookup[a]() : def();
};
單元測試範例。
describe('lookup a is x', () => {
it('doX', () => {
/* ... */
});
});
describe('lookup b is y', () => {
it('doY', () => {
/* ... */
});
});
describe('def', () => {
it('doZ', () => {
/* ... */
});
});
補上另一個範例,針對不同狀況顯示不同的樣版元件。
const renderer = (step) => {
const lookup = {
stepA: {
component: TestA,
config: { text: 'HelloWorld', number: 1 }
},
stepB: {
component: TestB,
config: { text: 'HelloWorld', number: 2 }
}
};
const Comp = lookup[step].component;
const config = lookup[step].config;
return <Comp {...config} />;
};
renderer('stepA')
重複使用 (Reuse)
重複使用程式碼可減少應用程式的複雜度。
對於一個應用程式來說
- 85% 的程式碼是用他人撰寫好的程式碼,例如:第三方套件 (一些工具)、框架 (解決瀏覽器相容性問題)。使用這些可重複使用的程式碼能讓開發者專注解決應用程式本身的功能。
- 15% 的程式碼是需要開發者開發的,開發者最好專注在這一塊。另外,在這 15% 內需要重複使用自己的程式碼,當同樣的功能寫第二次時,記得整理為一個函式來做重用。
因此,要降低程式碼的數量,要做到
- DRY (Don’s Repeat Yourself)
- 使用第三方套件
扇出 (Fade-out)
扇出 (fade-out) 是測量函式 (function / method) 直接或間接依賴模組或物件的數量。
- 函式的扇出數愈多,表示和愈多模組或物件有關連,也就是複雜度高、耦合度高,很可能導致 bug 愈多。
- 經由計算函式的扇出數與扇入數而得到的高複雜度的函式,表示這個函式做的事情太多,應該要重構、拆解。
- 函式若 <= 20 行,出錯的機率約為 28%;反之 78 %。
重構為可測試的程式
若函式的扇出數過高,表示和愈多模組或物件有關連,這樣在測試時要 mock 的東西就愈多,寫起測試大費周章 - 測試程式可能一點點,但前置作業非常累人。因此,重構此函式為可測試的程式。
- 透過注入 (injection) 與事件化 (eventing) 降低耦合度。
- 使用封裝成為外觀 (facade) 來降低扇出數。
扇入 (Fade-in)
扇入 (fade-in) 是測量函式使用其他模組或物件的數目,基本上愈大的扇入數愈好,表示愈多共用、重用性愈高。例如:寫 log、除錯 debug 紀錄等。
耦合 (Coupling)
耦合 (coupling) 表示模組或物件間的關聯狀況。
以下分六個等級,耦合度由高至低依序是
- 內容耦合 (content coupling):直接修改物件本身的狀態。耦合的分數是 5 分。
- 共用耦合 (common coupling):兩物件共用一全域變數,彼此就是共用耦合。耦合的分數是 4 分。例如:Redux vs React Hooks。
- 控制耦合 (control coupling):利用旗標 (flag) 或參數 (parameter) 的設定來控制物件的運作。耦合的分數是 3 分。
- 戳記耦合 (stamp coupling):經由參數傳遞一筆資料給某物件,而某物件會用到資料中的部份值做運算。耦合的分數是 2 分。
- 資料耦合 (data coupling):經由參數傳遞一筆資料給某物件,而某物件沒有得到控制權。耦合的分數是 1 分。
- 無耦合 (no coupling):無關,耦合的分數是 0 分。
實體 (Instantiation)
- 經由實體化產生的物件會有內容耦合與共用耦合的狀況,因此在用不到時必須摧毀
- 若系統內建立太多物件,必須考慮重構架構,減少複雜度。
相依性注入 (Dependency Injection)
定義
- 注入:處理物件的建構,並且將物件放入程式碼
- 模擬:測試時使用虛構的版本來替換原本的物件或方法來做呼叫
寫測試的步驟
- 找出相依物件
- 使用虛擬版本替換相依的物件
依賴反轉原則
Ref: wiki
註解 (Comments)
使用註解說明要測試什麼、要怎麼測試,可用工具 - JSDoc 或 Docco/Rocco。
人為測試 (The Human Test)
- 在非正式的狀況下,跟同事說明程式碼,可檢視其複雜度和找出 60 ~ 90% 的問題。
- 良好的註解可協助簡化程式碼。
本章重點回顧
- 測量程式碼的複雜度,並且試著做簡化,可讓程式碼更好讀易懂、好測試、減少 bug。
- 程式的模組間的相依性增加了測試的困難,但可用相依性注入的方式解決 - 找出相依物件、使用虛擬版本替換相依的物件即可。
- 跟同事說明程式碼、良好的註解可協助簡化程式碼。
- 找出程式碼的複雜度高的地方在哪兒、試圖重構,即可降低程式碼的複雜度。