《深入學習 JavaScript 模組化設計》

11 min read

主要依照自己的經驗紀錄心得,跟書中提到的內容有些許出入。

Rules of Good Code

Scalable

這裡指的是當負載上升,程式運算效能的變化,通常我們會考慮

Readable and Maintainable

模組化設計,如何提升程式碼的可讀性以及可維護性,過去 javascript 沒有原生的模組系統,早期會透過 IIFE 及 clousure 避免私有變數外溢,es6 後加入了 esm 作為原生的模組語法。

在 esm 中,每一個檔案都是一個模組,有自己的作用域 (scope),模組的作用域可以幫助我們

模組中公開暴露的變數及方法稱為介面,透過介面可以隱藏實作的複雜性,以常見的 modal component 為例,我們可以透過公開,且有語意的 props,隱藏對 a11y 功能實作的複雜性

<Modal
  lockFocus
  closeOnEsc
  closeOnOverlayClick
  blockScrollOnMount
/>

介面的好處:

模組化的原則

模組化設計是著重在寫出可讀、可維護、可測試程式碼的部分,下面會紀錄書中提到的概念。

單一職責原則 (SRP)

介面優先

何謂正確的介面,我認為這部分會因團隊而異。實際開發前,或許先找團隊成員進行 design review,團隊中一定有人更擅長技術,有人熟悉公司的商業邏輯,能夠針對實際需求及開發規範,權衡介面的彈性或是簡單性,何者需要是公開的 api,在之中取得共識才是最重要的。

找出正確抽象

太快或是盲目地遵循 DRY 原則 (don’t repeat yourself),很容易導致錯誤的抽象。

真實世界裡,程式碼是用來解決商業需求,隨著產品的迭代,過去 legacy code 裡的抽象,塞滿了不同的參數、條件判斷,這可能是因為當初有些看起來相似的情境,所以先做一層抽象,隨著產品成長以及商業模式改變,發現過去的抽象不足的部分,或是業務性質開始不同,但因為時程的關係,沒有時間給你重構,所以只好先加上新的 if else 檔一下,隨著團隊成員來來去去,下一個接手的人繼續參照著前人的智慧,最終這個抽象也演變成老子改不動的狀況。

DRY 的意義是協助寫出簡潔的程式,如果比較簡潔的程式比以前更難理解,那麼就先試著重複,當需求越來越明朗,或是有更好的模式出現,抽象可以明顯帶來好處時再去考慮。

這部分推薦可以參考 The Wet Codebase

一致 (Consistent)

書中提到的一致,我認為這部分比較偏向團隊的開發規範,這部分我認為 GitLab - Frontend Development Guidelines 是很好的參考範本

彈性 (Resilient)

彈性不一定是必須的,彈性會增加內部程式碼的複雜度,在設計 api 時也要不斷權衡彈性與簡單性。

書中有提到,可以透過多載 (overload) 的方式,維持簡單性也能兼顧彈性,以 axios 為例

// send a get request
axios('/user/12345');
 
// send a post request
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  },
});

如果你設計的函式參數超過 3 個以上,使用物件是一個不錯的選擇,除了不受參數順序影響外,物件屬性還可以描述該參數的用途。

明確性 (Unambigous)

函式的回傳型態,不應該受到輸入參數的影響,例如 fetch 始終會回傳一個 promise object,不會因為受到參數影響,與其他使用案例不一致。

簡單性 (Simple)

介面沒必要一開始就迎合所有可能的使用案例,應該先針對當下的需求製作一個簡單的解決方案,實作使用者還不需要的功能,可能會付出增加複雜性、難以維護,以及花費更多開發時間等等的代價。

如果 api 沒有要面對廣泛的使用者,可能只有自己的團隊會使用,維持精簡可以避免使用者以多種方式完成相同的工作,降低不確定性。

保持介面的簡單性,讓每個參數都有合理的預設值,以 fetch 為例,method 的預設值 GET,通常是最有可能被使用的 HTTP 動詞,隨著案例變複雜,也只要稍微調整 options 內的參數

fetch('/api/users');
 
fetch('/api/users', {
  method: 'DELETE',
  headers: {
    Authorization: 'Bearer ....'
  },
});

更小的介面 (Tiny)

盡可能維持小的介面

型塑內在

避免過度嵌套

當程式碼出現一系列的嵌套,很有可能已經出現流程控制與商業邏輯混在一起的情況,如果可以將邏輯隔離到各自的層級裡面,流程會變得更明確。

特徵糾纏與緊耦合

隨著模組成長,它也越容易錯誤地將不同的功能混在一起,要減少糾纏的風險,在設計時可以注意可以元件化,或以其他方式隔離可以清楚描述的關注點。

書中沒有提供相關程式碼範例,但我第一個直覺想到的是 redux 的分層,例如

擷取函式

根據條件定義變數時,可能會出現的情況

// ...
let website = null;
 
if (user.details) {
  website = user.details.website;
} else if (user.website) {
  website = user.websites;
}
// ...

將條件賦值以函式重構

// ...
const website = getUserWebsite(user);
// ...
 
function getUserWebsite(user) {
  if (user.details) {
    return user.details.website;
  }
 
  if (user.website) {
    return user.website;
  }
 
  return null;
};

組合與繼承

Reference