iT邦幫忙

2025 iThome 鐵人賽

DAY 8
1
Software Development

消除你程式碼的臭味系列 第 8

Day 8- 消除抽象層:直接存取資料,不要繞路

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250903/20124462P1N8QjGguI.png

消除你程式碼的臭味 Day 8- 消除抽象層:直接存取資料,不要繞路

抽象是個工具。
它的存在只有一個目的:管理複雜度。

當你的抽象層沒有隱藏任何複雜性,反而製造了更多追蹤和除錯的麻煩時,它就不是工具,而是腫瘤,必須切除。

過多的間接層(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); } // 總算有人做事了
}

https://ithelp.ithome.com.tw/upload/images/20250910/20124462hEH8PU4LYC.png

  • 它什麼都沒做: 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); 
  }
}

https://ithelp.ithome.com.tw/upload/images/20250910/20124462yH6XeK9teC.png

  • 誠實的依賴: UserController 清晰地依賴 userStore。它直接說明了「我需要用戶資料」。比那些虛假的抽象層有價值。

  • 行為靠近資料: getUserById 這個函式和它操作的 db.users 緊密相關。

  • 零成本抽象: 這個 userStore 甚至可以不是一個 class。一個簡單的函式模組就夠了。只有在需要狀態時才考慮 class

什麼時候需要抽象?

抽象層的存在必須回答一個尖銳的問題:「你到底隱藏了什麼?」

下面這張圖展示了最經典的情況:隱藏「可替換的具體實作」。
https://ithelp.ithome.com.tw/upload/images/20250910/201244623Ru0Al5Dr3.png
圖中的 Client (UserController) 依賴的是一個穩定的 IUserRepository 介面,而不是任何特定的資料庫實作。

  • 你有至少兩種具體的實作嗎? 在圖中,我們至少有兩種:生產環境用的 PostgresRepo 和測試環境用的 InMemoryRepo。抽象層完美地隱藏了「當下到底是用哪一種實作」這個變動點。如果你的系統從頭到尾只會有一種資料庫實作,那麼增加抽象層就沒有太大意義。

  • 你依賴的東西極度不穩定嗎? 比如一個第三方支付 SDK,API 每個月都在改。這時就該用一個 Adapter/Facade 模式的抽象層把它包起來,未來就算它再怎麼改,災難也只會發生在一個地方。反之,如果你的資料庫 API 穩定得像塊石頭,你就不需要為了「穩定」這個理由去增加抽象。

不符合就先別抽象;痛點真的出現,再加一層也不遲。

檢查清單

  1. 這一層是否有行為,還是只是改名與轉手?
  2. 這一層是否隔離了不穩定的外部依賴?
  3. 移除這一層後,呼叫端是否更簡單且更清楚?
  4. 是否能把邏輯移到更靠近資料的地方,縮短資料流?
  5. 觀測性與錯誤處理是否集中在 I/O 邊界?

今日重點

  • 抽象為了解決問題,不是為了堆疊層級。
  • 沒有行為的層就是間接層,能刪就刪。
  • 多實作、不穩定依賴、去重或語意提升才值得引入抽象。
  • 把行為放回靠近資料的地方,讓資料流直接、可追蹤、好除錯。

刪掉那些只會傳話(沒有行為)的無用層級。
讓資料流動的路徑盡可能短、盡可能直接。


上一篇
Day 7- 資料結構至上:好程式碼的關鍵思考
下一篇
Day 9- 迴圈最佳化:把邊界判斷和特殊處理移到外面
系列文
消除你程式碼的臭味12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言