程式碼審查時,常會遇到這種情況:打開檔案後,所有函式都在五行以內。
這是上一篇文章 程式碼異味 #2 的延續。
命名也很恰當,每個類別都只有一項職責。沒有什麼好挑剔的。
但當有人問「按下這個按鈕會發生什麼事?」時,卻沒人能立刻回答。
這類程式碼稱為 Ravioli Code(C2 原文)。
不只有負面意義
有趣的是,這個詞不總是用來批評。Ravioli 是由一個個小塊麵皮完整包住內餡的義大利麵。
它正是封裝良好的小型物件,也就是物件導向所追求的樣貌。實際上,也有人把 Ravioli 視為 Spaghetti 程式碼的反面。
問題在數量。
單一小塊都很完美,但盤子裡有 200 個,彼此以什麼順序、如何串接,卻沒有任何地方記載。
具體來說,會是這種樣子。
final class CheckoutCoordinator {
func start() { validator.validate(cart) }
}
final class CartValidator {
func validate(_ cart: Cart) { stockChecker.check(cart.items) }
}
final class StockChecker {
func check(_ items: [Item]) { priceCalculator.calculate(items) }
}
final class PriceCalculator {
func calculate(_ items: [Item]) { paymentPreparer.prepare(items) }
}
// ... 還有 16 個這樣的類別
每個類別都無可挑剔。命名精確,工作也只有一項。
但沒有任何檔案了解完整的付款流程。
流程只存在於類別之間的呼叫關係中,不是讀程式碼就能知道,而是必須接上除錯器。
為什麼會拆得過細
把「函式越短越好」當成規則時。《Clean Code》甚至主張函式應只有兩、三行,最長也只能四行。
這項建議真正要達成的,其實是讓一個函式只處理一個抽象層級。但當它被換算成行數,目的就消失了。
結果是 30 個五行函式,其中 28 個只在一個地方被呼叫。
名稱無法摘要內容時。、handleUserAction、processData、updateState這類名稱,無法告訴你它們做了什麼。
拆成這種命名的函式後,讀者終究得打開函式本體,而且拆得越多,要打開的地方就越多。
好的抽取應該讓人不必查看本體,結果反而適得其反。
**抽象層級混在一起時。**同一個函式裡,同時出現「驗證訂單」這種政策層級的句子,以及「將索引加一」這種細節層級的句子。讀者的視線就會不斷上下移動。
在這種狀態下貿然拆分,只會產生抽象層級混亂的片段。
**為了預備重用而抽出只使用一次的程式碼時。**這和製造 Lasagna 程式碼是同一種心理,只是方向從垂直改成水平。
成本會以跳轉次數呈現
Ravioli 程式碼的成本可用一句話概括:為了回答一個問題,要打開幾次檔案?
讀程式碼時,人會把流程暫存在短期記憶中。超過三、四個步驟後,這份記憶就會變模糊。
在檔案之間跳轉六次後,一開始想找什麼會變得模糊,只好回到起點。
反覆發生後,直接執行確認反而比閱讀程式碼更快。
副作用也會隨之而來。
- 加入新功能時找不到既有片段,只好再做一個相似的。重複內容悄悄增加。
- 找不到修正錯誤的位置,只好在流程末端,也就是畫面端,加入臨時處理。
- 完整流程沒有寫在程式碼裡,只存在文件或某人的腦中。那個人離開團隊後,流程也會一併消失。
找回流程的方法
**建立一個記錄流程的位置。**這是最有效的措施。
放置一個能一眼看見完整順序的函式,讓它只描述順序。
func checkout(_ cart: Cart) async throws -> Receipt {
try validate(cart)
try await reserveStock(cart.items)
let amount = calculateTotal(cart)
let payment = try await charge(amount)
return try await confirm(cart, payment)
}
細節仍留在各自的位置。改變之處只是流程被寫在同一個畫面上。
這類函式也稱為協調器。Ravioli 程式碼缺少的正是這個位置。
**每一層只放一個抽象層級。**上方函式的五行都是同一高度的句子。若混入items.count > 0這類細節條件,抽象層級就會被破壞。
讀完一個函式後,確認句子是否處於同一個視線高度,比遵守拆分標準更有用。
**是否抽取由名稱決定。**標準不是行數,而是這個問題:「為這個片段取的名稱,是否比本體透露更多資訊?」
把if user.age >= 19抽成isAdult(user)能顯示意圖,因此有益。把array.append(item)抽成addItem則毫無收穫。
**相近的東西就放在一起。**會一起變動的程式碼,放在同一檔案、同一資料夾。
若為了遵守一個檔案一個型別的慣例,反而讓總是一起開啟的型別分散,鄰近性比慣例更重要。
**服務單位也適用相同標準。**過度切分微服務的狀態稱為奈米服務。
處理一個請求卻要經過 12 個服務,是 Ravioli 程式碼延伸到網路另一端的形式。此時,跳轉成本還會加上延遲與故障點。
Lasagna、Ravioli,以及 Spaghetti
把三者放在一起,可以整理如下。
| 形式 | 片段狀態 | 問題 |
|---|---|---|
| Spaghetti | 龐大且糾結 | 無法預測執行流程 |
| Lasagna | 垂直分層 | 一次變更就得貫穿多個層次 |
| Ravioli | 細小且整潔 | 片段之間的流程無處可尋 |
三者的掌握整體的成本都很高,只是方式不同。
若只用每個片段的美感衡量程式碼品質,就看不出 Lasagna 與 Ravioli。因為兩者在片段層級都拿滿分。
總結
- Ravioli 程式碼是小而整潔的片段過多,導致沒人能說明整體流程的程式碼。
- 它也曾被用來正面指稱封裝良好的程式碼。問題出在數量。
- 主要原因是行數規則、模糊的命名,以及混雜的抽象層級。
- 解法的核心,是設置記錄流程的位置。
- 抽取標準不是長度,而是名稱。只有名稱能比本體說明更多時才抽出。
下一篇是完全相反的極端:不是片段太多,而是只有一個。
我們來看看一個類別就了解整個 App 的 God Object。
來源與確認標準
- Ravioli Code — C2 Wiki · 原作者文章 · 確認 2026-08-17 · 依據:小型且封裝的物件過度分割成 Ravioli 程式碼的比喻

![[程式碼異味 #3] Ravioli 程式碼,沒人知道流程的程式碼 封面圖](/assets/images/posts/6faf8840-1640-4930-a94c-3ed202b71075/ravioli-code-disconnected-objects.jpg)