# 抽象層級的紀律，每層清楚的做一件事情

> 每層只負責一件講得清楚的事，責任模糊時 debug 就得跨層猜。談抽象不足、過度抽象，以及沒有第二個 caller 就別先抽介面。

原文：https://sheng.page/posts/abstraction-layers-one-responsibility-each/ · 發布：2025-03-08 · 作者：Sheng

分層這件事，講起來人人會點頭，做起來幾乎每個專案都歪。歪的方向通常兩種：要嘛抽過頭，為了某個還沒發生的需求先疊三層；要嘛根本沒抽，I/O、商業邏輯、呈現全擠在同一個函式裡。兩種都讓你付一樣的代價，出事的時候得跨好幾層去猜問題在哪。我自己的判準很單純，一層只負責一件講得清楚的事，講不清楚就是還沒切好。

## 責任模糊，debug 就變成考古

分層的價值不在「看起來有架構」，在於出事時你知道該翻哪一層。如果每一層的責任你都能用一句話講完，那 bug 一出現，你大概就知道它住在哪。反過來，責任含糊的系統 debug 起來像考古，得一層一層挖，因為你根本不確定資料在哪裡被改壞、邏輯在哪裡轉錯。

抽象不足是最常見的起點。一個 handler 函式，裡面同時做了參數解析、開 DB 連線查資料、套商業規則算結果、最後拼 HTML 回去。剛寫的時候很爽，一個檔案全看得到，改一個小地方超快。問題是這種函式長到某個程度就動不了，因為你想改算錢的規則，卻被迫同時面對 SQL 跟 HTML，任何一處手滑都可能噴在意想不到的地方。我接過這種程式，光是要搞清楚某個欄位到底在哪一段被覆寫，就花掉一個下午。責任沒切開，每次改動的影響範圍就是整個函式，不是你動到的那幾行。

但反過來矯枉過正更難救。

## 過度抽象，是為了「以後可能」付現在的稅

過度抽象大概是這個樣子：因為「以後說不定要支援別的金流／別的資料源／別的通知管道」，所以現在先抽一個 `PaymentProvider` 介面、一個 `Repository` 抽象、一個 `Notifier` 工廠，結果整個專案只有一個實作。每次要改一個很簡單的東西，得穿過介面、穿過工廠、穿過某個 registry，才摸到真正在跑的那二十行。我做 SvelteKit SSR 那次就是這樣，環境裡 Vite 轉換慢，我為了「正確地」處理它，疊了一堆中介層去包，最後每次 debug 都在隧道裡爬。後來整個拆掉改純 SPA，自己寫八十行 hash router，反而五分鐘定位得到問題。

這裡的關鍵是 anti-additive 的直覺：沒有第二個 caller，就別先抽介面。介面是用來整合「已經存在的重複」，不是用來預約「未來可能的重複」。你先寫直白的版本，把邏輯老老實實列在那，等真的冒出第二個、第三個用法，重複在哪裡自己會浮出來，那時候再抽，你會抽得準。提前抽的介面幾乎都猜錯邊界，因為你是在沒有第二個案例的情況下想像共通點，想像出來的共通點通常是錯的，最後變成每個實作都得繞過它。

過度抽象還有個隱性成本：它讓人失去對底層的直覺。包太多層之後，寫的人漸漸不知道一個請求實際上碰到了哪些東西、HTTP 在哪裡結束、資料最後寫到哪裡。出事的時候，他 debug 的是自己疊出來的抽象，不是真正在跑的系統。

## 好的分層，是只換一層就好

判斷分層切得好不好，有個很實際的測試：當你要換掉某個東西，需要動幾層。換 DB 從 PostgreSQL 到別的，如果你的資料存取藏在一層後面，理想上只改那一層，上面的商業邏輯一行都不用碰。換 tracing 後端、換 cache、換掉某個外部 API，道理一樣。能做到只換一層，意味著那層的責任確實劃清楚了，這跟介面驅動其實是同一件事的兩種講法，介面就是「我只承諾這層對外的行為，內部怎麼換是我的事」。

將資料存取與商業邏輯分開，是因為它們的責任不同。好換 DB 是分開後得到的好處，不該拿來當抽象的起點。為想像中的可替換性預先抽一層，通常只會掉回上一段的坑。

層數沒有標準答案，每層的責任能不能用一句話交代才重要。三層且責任分明的系統，通常比五層互相重疊，或所有事都擠在同一層容易維護。


## 補充筆記

- 每層只負責一件講得清楚的事，責任講不清楚就是還沒切好。
- 抽象不足讓單一函式的改動波及全域；過度抽象讓簡單修改要穿過好幾層。
- 沒有第二個 caller 就別先抽介面，等重複在哪裡浮出來再動手才會準。
- 好分層的指標是「只換一層就好」，這是責任劃清楚之後的好處，不是抽象的目的。
- 層數漂不漂亮不重要，重點是每層都能一句話講清楚它幹嘛、不幹嘛。
> 想法與技術判斷出自 Sheng，和 Claude 一起起草 · 列入 20260613 blog 翻新計劃，新漆未乾。