Day 11-13 拆解的三個案例(空的 UnitOfWork、假的重試、死碼 DTO),都有一個共同點:單獨看,它們都「沒有 bug」——執行起來不會拋例外,測試會通過,甚至邏輯順著讀下去也說得通。如果 review 的標準只有「有沒有明顯錯誤」,這三個案例全部會過關。
但這個系列走到這裡,想帶出一個更難量化、卻同樣重要的問題:一段程式碼「沒有 bug」,不代表它沒有成本。 它花掉的是另一種成本——認知負荷(cognitive load)跟維護成本,這兩者不會反映在測試報告或 bug 數量上,卻真實地決定了半年後這個系統改不改得動。
認知負荷指的是,理解一段程式碼在做什麼,需要讀者的大腦同時記住、追蹤多少個「移動的部分」。版本 A 的 publish() 呼叫鏈是:PublishArticleService::publish() → Article::publish() → ArticleRepository::save(),三步,讀者同時要記住的物件只有 Article 一個。
版本 B 的同一個操作,呼叫鏈是:PublishArticleService::publish() → runInTransaction() 包住 → CommandBus::dispatch() → PublishArticleCommandHandler::handle() → ArticleAggregate::publish() → EventDispatcher::dispatch()(觸發 ArticlePublishedEvent,被 LoggingEventListener 接住)→ ArticleRepository::save()(依序穿過 TransactionalArticleRepositoryDecorator → RetryingArticleRepositoryDecorator → CachingArticleRepositoryDecorator → LoggingArticleRepositoryDecorator → ArticleTableGateway)。要完整理解這一次 publish() 呼叫的行為,讀者必須同時在腦中維護至少 9-10 個物件的狀態跟彼此的呼叫關係。
這不是「難不難」的問題,是人類工作記憶本來就有限,超過負荷的部分不是被記住,而是被略過。 而被略過的部分,正是 SqliteUnitOfWork 沒有真的做交易保護、RetryingArticleRepositoryDecorator 永遠只試一次這類細節最容易被藏住的地方——不是因為它們刻意隱藏,而是因為讀者的注意力早就被前面七、八層呼叫消耗完了。
「有沒有 bug」是一個二元判斷,而且只反映「現在」這個時間點的狀態。但工程實務裡真正累積起來的成本,是「未來每一次修改,要花多少力氣」——這正是 Day 13 提到的量化實驗想回答的問題:新增一個「分類」欄位,乾淨版本改 3 個檔案,過度設計版本(僅計有被呼叫的部分)要改 10-11 個檔案。
❌ 只用「有沒有 bug」衡量程式碼品質:
這個 PR 跑起來沒有錯誤、測試都過
→ 品質沒問題
✅ 同時用改動幅度衡量維護成本:
這個功能只有 4 條業務規則,
下一次新增一個欄位,預期要改幾個檔案?
如果答案是兩位數,問題已經存在,只是還沒發作。
版本 B 的 92% 死碼,更是這個問題的極端案例:這些程式碼此刻不會被執行、不會被測試碰到,所以「有沒有 bug」這個問題根本無從問起——但它們仍然佔用著每一次搜尋、每一次重構、每一次新人 onboarding 時必須先排除的認知空間。這是一種「沒有 bug、卻持續在收租」的成本,而且沒有人在為它記帳。
Day 15 已經講過,92% 死碼這種規模問題,光憑肉眼幾乎不可能在短時間內確認。認知負荷跟維護成本又比死碼比例更難量化——它們不是「對或錯」的二元判斷,而是連續的、累積的效果,今天看起來還好,半年後疊加起來才會顯現。這正是這個系列要在第三部轉向「把期待寫成可執行規則」的原因:認知負荷跟維護成本雖然難以精確衡量,但它們的一部分代理指標——呼叫鏈深度、改動檔案數、死碼比例——是可以被量化、被自動檢查的。 這呼應這個系列反覆強調的主題句:AI 沒有發明過度設計,它只是讓過度設計的速度追上了你按下 Enter 的速度;Review 要跟得上,審的就不能再是程式碼本身,而是產生程式碼的規則。 這裡的「規則」,正是把認知負荷、維護成本這類抽象但真實的成本,轉換成具體可以被自動驗證的數字門檻。
如果請你為你目前的專案定義一個「維護成本」的代理指標(例如平均改動一個欄位要動幾個檔案、平均呼叫鏈深度),你會選哪一個指標?這個指標現在有沒有被追蹤?
publish() 只需追蹤 1 個物件,版本 B 需要追蹤 9-10 個第二部的案例拆解到這裡告一段落。Day 17 開始進入第三部解法篇,回到 Outside-In TDD/ATDD 這套十幾年前就存在的紀律,講清楚它本身就是最古老的過度設計解藥。
寫這篇的過程中,我一直想找一個乾淨俐落的公式來定義「認知負荷」,但寫著寫著發現自己在硬湊。認知負荷這種東西,本質上就是模糊的、因人而異的——資深工程師讀版本 B 的呼叫鏈可能比新人快一些,但快多少、快在哪裡,很難給出一個放諸四海皆準的數字。這反而讓我更確定一件事:正因為這類成本天生模糊,才更需要找出可以被量化的代理指標,而不是繼續依賴「讓資深的人多看幾眼」這種同樣模糊的解法。這個結論,某種程度上也是我自己想清楚之後才寫出來的,不是動筆前就有的答案。