介面越小,被誤用的空間越小,維護成本越低。
公開的每個函式,都是欠下的一筆債
介面是你經過深思熟慮後,願意用未來十年的維護成本來做擔保的一個最小化承諾。
每一次輕率地公開一個內部實作,就在為未來埋下一顆地雷。
總有一天,當要重構時,會發現自己動彈不得,因為有十個地方依賴了那個當初根本不該公開的地方。
介面是你唯一的承諾
目標很單純
// 🔴 臭味道:把所有東西都 export
export function toDto(post) { return { id: post.id, title: post.title }; }
export function logDebug(msg) { console.log('[debug]', msg); }
export function fetchPosts(db) { return db.posts.all(); }
呼叫端可以動到不該碰的東西,耦合度過高。
喪失了自由: 有人開始直接使用 toDto
。現在,你想在 DTO 裡加一個 author
欄位。恭喜,所有直接呼叫 toDto
的地方都可能出錯。被自己的實作細節給綁架了。
提供了錯誤的工具: 使用者根本不該關心 toDto
或 logDebug
。他們只想「取得文章列表」。他們可能用這些零件組裝出奇怪的東西。
契約過於龐大: 你現在必須為這 3 個函式的穩定性負責,而不是 1 個。你的維護成本無緣無故增加了 3 倍。
好的模組就像一個黑盒子。
外界不需要、也不應該知道裡面發生了什麼。
你只需要提供一個定義良好、功能單一的入口。
// 🟢 好品味:這才叫 API。一個入口,一個承諾。
// 這些都是內部實作細節,外界一個字都不該看到。
// 可以把它們全部改名、合併、刪除,而不需要通知任何人。
function toDto(post) { return { id: post.id, title: post.title }; }
function logDebug(_msg) { /* ... */ }
function fetchPostsFromDb(db) { return db.posts.all(); }
// 這是我唯一對外的承諾。
export function listPublishedPostTitles(db) {
logDebug('Fetching posts...');
const posts = fetchPostsFromDb(db);
return posts.map(toDto);
}
自由: 現在擁有 100% 的內部重構自由。
只要 listPublishedPostTitles
的輸入和輸出不變,可以把 toDto
、logDebug
、fetchPostsFromDb
重寫一百遍。
意圖清晰: API 的名字 listPublishedPostTitles
直接說明了它的業務目的。使用者一看就知道這是做什麼的。而 fetchPosts
、toDto
只是實作細節,不是業務目的。
最小化攻擊面: 更少的公開介面意味著更少的 bug 來源,更少的誤用可能,以及更簡單的測試。
先問自己這幾個問題:
預設就是 private
: 每個函式、每個變數,預設都是模組內部的。必須給出一個極其強大的理由,才能將其公開。理由不是「可能有用」,而是「絕對必要」。
公開意味著永恆: 一旦公開,就幾乎永遠無法在不破壞別人程式碼的情況下修改或刪除它。有打算對這個函式簽名負責到專案終結嗎?
提供解決方案,而非工具箱: 不要給使用者一堆樂高積木(工具函式),然後讓他們自己去拼。直接給他們一輛組好的法拉利(解決特定問題的高階函式)。
把介面縮到最小,減少誤用與耦合,停止把內部亂七八糟的實作細節當成 API 到處亂丟。
設計一個乾淨、最小化、且有意義的介面,這就是好程式碼的差別。