抽象是個工具。
它的存在只有一個目的:管理複雜度。
當你的抽象層沒有隱藏任何複雜性,反而製造了更多追蹤和除錯的麻煩時,它就不是工具,而是腫瘤,必須切除。
過多的間接層(Indirection)只會讓程式碼難以追蹤、效能更差、除錯更痛苦。
每一層只是換個名字、原封不動傳遞參數,沒有任何行為,也沒有隔離不穩定性。
// Controller -> Service -> Repo -> DB (每一層都在假裝自己有價值)
class UserController {
constructor(service) { this.service = service; }
get(id) { return this.service.get(id); } // 傳話
}
class UserService {
constructor(repo) { this.repo = repo; }
get(id) { return this.repo.get(id); } // 繼續傳話
}
class UserRepo {
constructor(db) { this.db = db; }
get(id) { return this.db.users.find(id); } // 總算有人做事了
}
它什麼都沒做: UserService
在這裡的價值是零。它只是個傳話筒。如果你把它刪了,除了讓呼叫鏈更短之外,什麼都不會改變。
虛假的解耦: 你以為 UserController
它透過一連串的傳話筒,最終還是依賴資料庫的行為和結構。這種抽象只隱藏了依賴關係,卻沒有消除它。
除錯地獄: 當 db.users.find(id)
出錯,你會得到一個從 UserController
開始,貫穿三層無用程式碼的呼叫堆疊。為了追蹤一個簡單的資料庫查詢,你得看一堆多餘的。
程式碼應該直接、透明。
如果你的控制器需要從資料庫拿用戶資料,那就讓它直接去拿,或者透過一個真正處理資料邏輯的模組去拿。
// userStore.js —— 一個只關心「資料」的模組
export function getUserById(db, id) {
// 這裡可以放快取、資料驗證、組合等「真正」的資料邏輯
return db.users.find(id);
}
// userController.js —— 直接、誠實地說明它的意圖
import { getUserById } from './userStore.js';
export class UserController {
constructor(db) { this.db = db; }
get(req) {
// 它需要用戶資料,所以它呼叫了處理用戶資料的函式。沒有廢話。
return getUserById(this.db, req.params.id);
}
}
誠實的依賴: UserController
清晰地依賴 userStore
。它直接說明了「我需要用戶資料」。比那些虛假的抽象層有價值。
行為靠近資料: getUserById
這個函式和它操作的 db.users
緊密相關。
零成本抽象: 這個 userStore
甚至可以不是一個 class
。一個簡單的函式模組就夠了。只有在需要狀態時才考慮 class
。
抽象層的存在必須回答一個尖銳的問題:「你到底隱藏了什麼?」
下面這張圖展示了最經典的情況:隱藏「可替換的具體實作」。
圖中的 Client
(UserController
) 依賴的是一個穩定的 IUserRepository
介面,而不是任何特定的資料庫實作。
你有至少兩種具體的實作嗎? 在圖中,我們至少有兩種:生產環境用的 PostgresRepo
和測試環境用的 InMemoryRepo
。抽象層完美地隱藏了「當下到底是用哪一種實作」這個變動點。如果你的系統從頭到尾只會有一種資料庫實作,那麼增加抽象層就沒有太大意義。
你依賴的東西極度不穩定嗎? 比如一個第三方支付 SDK,API 每個月都在改。這時就該用一個 Adapter/Facade 模式的抽象層把它包起來,未來就算它再怎麼改,災難也只會發生在一個地方。反之,如果你的資料庫 API 穩定得像塊石頭,你就不需要為了「穩定」這個理由去增加抽象。
不符合就先別抽象;痛點真的出現,再加一層也不遲。
刪掉那些只會傳話(沒有行為)的無用層級。
讓資料流動的路徑盡可能短、盡可能直接。