測試與程式碼品質

[程式碼異味 #2] 千層麵程式碼:為什麼新增一個欄位要修改七個檔案

即使整齊地分層,新增一個欄位仍需修改七個檔案的程式碼,就是千層麵程式碼。本文整理層數增加的原因、成本產生的位置,以及何時需要保留或移除分層。

閱讀 6 分鐘
[程式碼異味 #2] 千層麵程式碼:為什麼新增一個欄位要修改七個檔案 封面圖

上一篇看到的義大利麵程式碼,是流程彼此糾纏的程式碼。那麼,只要把流程完美整理好,就會變成好程式碼嗎?

這是接續上一篇文章 程式碼異味 #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)警告的正是這種情況。

**團隊變大後按層次拆分時。**正如康威定律所說,組織的溝通結構會原封不動地刻在架構中。

團隊有四個,層次也很容易變成四個,而且邊界往往遵循人的邊界,而非技術需求。

新增一個欄位時,從 DTO 到畫面需修改的七層路徑圖
紅色方框是什麼都不決定的傳遞層

成本從哪裡產生

層次太多究竟哪裡不好,讓我們具體指出來。

**變更成本與層數成正比。**一個欄位就要改七個檔案。更可怕的是,開發者會試圖避開這件事。

不按規則逐層傳遞,而是在 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 · 依據:分層的目的與變更邊界

延伸閱讀