上一篇看到的義大利麵程式碼,是流程彼此糾纏的程式碼。那麼,只要把流程完美整理好,就會變成好程式碼嗎?
這是接續上一篇文章 程式碼異味 #1的內容。
不一定。
把層次整齊分開、規定每一層只能呼叫下一層,並用介面劃出邊界,程式碼仍可能變得讓人不想碰。
只是要在畫面顯示一個字串欄位,就得修改七個檔案的程式碼。這稱為千層麵程式碼(Lasagna Code)(C2 原文)。
層層堆疊得很漂亮的程式碼
就如同名稱所示。千層麵是將麵皮與醬汁一層層堆疊製成的。
剖面整齊、層次分明。問題是,你無法只取出其中一層。
來看一個典型例子。伺服器回傳的使用者資料新增了nickname欄位,只要將它顯示在畫面上即可。
1. UserDTO — 在伺服器回應結構中新增欄位
2. UserMapper — DTO在轉換成領域模型的程式碼中新增一行
3. User — 在領域模型中新增屬性
4. UserRepository — 協定簽章變更時,這裡也要修改
5. FetchUserUseCase — 明明只是傳遞,卻因型別受阻而修改
6. UserViewModel — 再次轉換成畫面模型
7. ProfileView — 終於顯示
從 DTO(Data Transfer Object,資料傳輸物件)到畫面為止,同一個字串會被放入三種不同型別。
七個檔案中,真正做決策的地方有幾個?通常只有一兩個。
其餘只是原封不動地把值往旁邊傳遞。
這種層稱為傳遞層(pass-through layer)。
在架構模式文獻中,請求不做任何處理、只穿過各層的狀態,也稱為沉洞反模式。這正是千層麵程式碼的核心症狀。
為什麼會變成這樣
沒有人是出於惡意才這樣做。事實上,通常正好相反。
層數增加,多半是因為脫離脈絡地套用好建議。
**把「分離層次」當成規則時。**看到 Clean Architecture 圖,就依照圓圈數建立資料夾。
那張圖不是規定應該有幾層,而是描繪相依性只能朝內的原則(Presentation-Domain-Data Layering)。
但轉換成資料夾結構後,意義就變了。
**把「依賴抽象而非實作」套用到所有地方時。**就會產生大量只有一個實作的協定。
UserRepository一個協定和UserRepositoryImpl一個實作。這個協定的作用,只是多增加一次程式碼跳轉。
如果目的是在測試中插入假實作,這有其價值;但若沒有這個計畫,就只是多了一層。
**事先為了「之後可以更換」而準備時。**因為資料庫可能變更而抽象化,也因為伺服器可能變更而再包一層。
YAGNI(You Aren’t Gonna Need It)警告的正是這種情況。
**團隊變大後按層次拆分時。**正如康威定律所說,組織的溝通結構會原封不動地刻在架構中。
團隊有四個,層次也很容易變成四個,而且邊界往往遵循人的邊界,而非技術需求。
成本從哪裡產生
層次太多究竟哪裡不好,讓我們具體指出來。
**變更成本與層數成正比。**一個欄位就要改七個檔案。更可怕的是,開發者會試圖避開這件事。
不按規則逐層傳遞,而是在 ViewModel 直接呼叫 API 的捷徑就會出現。
規則難以遵守時,就會出現繞過規則的程式碼。最後層次依然存在,流程卻變成義大利麵。千層麵生出了義大利麵。
**閱讀成本很高。**要知道某個值從哪裡來,必須沿著層次往下追。
每一層都短而清楚,但跳過七次之後,你會忘記最初的問題是什麼。
**除錯會變慢。**堆疊追蹤變長,要找出值在哪一層出錯,就得在每層設置中斷點。
**建置會變慢。**如果把層次拆成模組,尤其如此。每次修改下層,都會重新建置所有上層。
何時需要分層,何時不需要
這不代表要消除所有層次,而是需要判斷標準。
有一個問題通常很好用。
「這一層是在做決策,還是只是在傳遞?」
做決策表示進行轉換、驗證、分支或組合其中一項。將伺服器日期字串轉成Date的 mapper 就是在做決策。
有快取就使用快取、沒有就使用網路的儲存庫也在做決策。相反地,原樣接收並傳遞參數的 UseCase 什麼都沒決定。
另一個標準是變更原因。伺服器回應格式變更,與畫面顯示規則變更,是不同的原因。
因此 DTO 與畫面模型有分開的價值。反之,如果領域模型與畫面模型總是一起變更,就沒有分開的理由。
整理成表格如下。
| 情況 | 是否應該分層 |
|---|---|
| 外部回應格式與內部模型各自變化 | 是 |
| 確實有替換實作的計畫 | 是 |
| 測試需要假實作 | 是 |
| 規模很大,需要將團隊邊界刻進程式碼 | 有條件地是 |
| 只有一個實作,未來也只有一個 | 否 |
| 只接收值並原樣傳遞 | 否 |
| 因為圖中有這一層就建立了它 | 否 |
如何移除層次
處理已經堆好的千層麵,大致可以依照以下順序進行。
**先找出傳遞層。**如果方法本體只有一行,而且該行呼叫其他物件的同名方法,就是候選。
在整個專案搜尋這個模式,很快就能列出清單。
**計算實作數量。**查看每個協定有幾個實作。如果只有一個,測試也沒有使用,就刪除協定,直接使用具體型別。
這時最好不要以效能作為理由。any UserRepository透過相同的存在型別呼叫,確實會經過 witness table。
但若把協定作為泛型限制,經過特化後這項成本就會消失。相反地,final不是的類別,即使用具體型別呼叫,預設仍是動態派送。
而且在跨越網路或資料庫的儲存庫呼叫中,這個差異不會反映在實測結果上。
移除協定的理由不是效能,而是跳轉次數。
**合併型別。**如果 DTO 與領域模型的欄位完全相同,且總是一起變更,就使用一個型別。
也有人認為將Codable直接附加到領域模型是污染。但應先判斷這種污染何時會成為問題。
**在同一層內合併。**如果不想刪除層次,可以保留層次但合併檔案。
只要把協定與實作放在同一個檔案,就能減少跳轉次數。
總結
- 千層麵程式碼是層次過多,導致小變更也必須修改多個檔案的程式碼。
- 通常不是因為寫了糟糕的程式碼,而是因為脫離脈絡地套用了好建議。
- 核心症狀是傳遞層:什麼都不決定,只把值往旁邊傳遞的層。
- 判斷標準有兩個:這一層是否做決策,以及它是否因不同於其他層的原因而變更。
- 規則難以遵守時就會出現繞路。層次過多,最後義大利麵也會一起長出來。
下一篇要談的,是問題不在層次而在碎片的情況。每個類別都很小、封裝良好,卻沒有人能說明整體流程的程式碼——拉維奧里程式碼。
來源與確認標準
- Lasagna Code — C2 Wiki · 作者原文 · 確認 2026-08-17 · 依據:千層麵程式碼的比喻與過度分層問題
- Presentation-Domain-Data Layering — Martin Fowler · 作者原文 · 確認 2026-08-17 · 依據:分層的目的與變更邊界

![[程式碼異味 #2] 千層麵程式碼:為什麼新增一個欄位要修改七個檔案 封面圖](/assets/images/posts/c81880b5-e5ed-45e7-8f84-04d1a59bc481/lasagna-code-layered-architecture.jpg)