第 4 / 6 部 待錄

行為與控制流

《實作模式》Podcast 準備稿:行為與控制流

書名: 實作模式 Implementation Patterns 作者: Kent Beck 系列: 設計品味系列(程式碼層次的設計直覺) 公開標題建議: 設計品味系列|Implementation Patterns.第四集:主流程、訊息,與 Guard Clause (4/6) 涵蓋範圍: 第 7 章「行為 Behavior」(含 Main Flow、Message、Guard Clause、Exception 等模式)

背景速覽

上一集講完「狀態」——資料怎麼存、怎麼溝通。這一集是另一面:行為,也就是控制流程。Java 繼承了 von Neumann 的核心隱喻——一連串依序執行的指令。這一章談的是:在「順序、條件、迴圈、訊息、例外」這幾種控制機制裡,你怎麼選、怎麼排,才能讓讀者先看懂大局、再鑽細節。這章有兩個明星模式:用訊息(多型)取代條件式,以及用 Guard Clause 馴服巢狀 if。

一句話重點

清楚地表達「主流程」,然後用例外和 Guard Clause 把不重要、少執行的路徑推到旁邊去——別讓「很少發生的事」遮住「天天被讀的事」。

值得討論的重點

重點一:Main Flow——為「常被讀的部分」優化,不是為「邊角情況」

Kent Beck 說,程式設計師心裡通常都有一條程式的主流程:處理從這裡開始、在那裡結束,中間可能有決策和例外,但有一條主路徑可循。

關鍵的價值判斷在這裡:把程式語言的表達力,拿去清楚表達「很少執行、很少變更」的邊角情況,會遮蔽掉程式裡更有槓桿的部分——那些經常被讀、被理解、被修改的部分。 例外條件不是不重要,而是「聚焦把主流程講清楚」這件事更有價值。

這個觀點看似簡單,但它是後面所有模式的指導原則。當你寫一段控制流,先問自己:哪條是主流程?然後用最直接的方式把它寫清楚,讓所有的錯誤處理、邊界情況退到背景去當配角。一段程式碼如果把主流程和八種錯誤處理「平等對待」,讀者就得做考古學——「到底哪幾行會真的跑到?」

重點二:Message——用「發訊息」承認「變化是常態」

這是物件導向相對於程序式最核心的一躍。程序式語言用 procedure call 當資訊隱藏:input(); process(); output(); 說的是「要懂這個運算,你只要知道它由這三步組成,細節先別管」。

物件導向的訊息更進一步,它多說了一層:對每個方法,潛在地存在「一整組結構相似但細節不同」的運算,而且你不必在寫不變部分時,就把所有未來的變化釘死。 用 Kent Beck 的話:程序式的版本說「那邊有個東西,細節不重要」;訊息的版本說「故事到這裡,有件跟 input 概念相關的有趣事情會發生,而細節可能會變」。

他把這個推到極致是 Choosing Message——用多型訊息取代 case 語句。大量用 choosing message 的程式碼,幾乎沒有明確的條件式;每個 choosing message 都是對未來擴充的邀請,而每個明確的條件式都是一個「之後要手動改才能改變整體行為」的點。代價是讀者要在多個類別之間跳,所以你得用意圖揭示的方法名稱幫他導航。他也誠實提醒:注意 choosing message 何時是多餘的——如果某個計算根本不可能有變化,就別為了「提供變化的可能」硬塞一個方法進去。這跟前面的「彈性是爛設計的藉口」一脈相承。

重點三:Guard Clause——拒絕「單一出口」的老教條

這是整章我最想推薦的實用模式,因為它能立刻改善你今天的程式碼。比較這兩段:

巢狀版本:

Server server = getServer();
if (server != null) {
  Client client = server.getClient();
  if (client != null) {
    Request current = client.getRequest();
    if (current != null)
      processRequest(current);
  }
}

Guard Clause 版本:

Server server = getServer();
if (server == null) return;
Client client = server.getClient();
if (client == null) return;
Request current = client.getRequest();
if (current == null) return;
processRequest(current);

第二段把「前置條件不滿足就提早 return」一條條列出來,主流程(processRequest)清清楚楚站在最後,沒有任何巢狀。Kent Beck 點破關鍵:if-then-else 表達的是「同等重要的兩條替代流程」;Guard Clause 表達的是「一條比另一條更重要」。當你的兩條路不對等——一條是正常、一條是「這裡有問題,閃」——就該用 guard clause。

他還直接挑戰一條老教條:「每個程式應該單一入口、單一出口」。他說這在 FORTRAN 和組合語言時代有道理,但在 Java——方法小、資料大多局部——這條規則太保守,而且會擋掉 guard clause。這是很值得在節目裡展開的點:很多「最佳實踐」其實是特定時代、特定語言的產物,換了環境就該重新審視。

重點四:Exception——強大,但是一種「設計洩漏」

例外適合表達「跨越多層函式呼叫的跳躍」:你在呼叫堆疊很上層才發現磁碟滿了,但只能在很下層才合理地處理它。在發現點丟例外、在能處理的點接,遠比在每一層中間程式碼都塞檢查好。

但 Kent Beck 對例外的態度相當保留,他給了一個很犀利的說法:例外是一種「設計洩漏」(design leakage)——一個被呼叫方法丟出的例外,會影響所有可能呼叫它的方法的設計。它讓追蹤控制流變難,因為相鄰的語句可能跑在不同的方法、物件、甚至套件裡。他下了一條明確的優先順序:

盡可能用序列、訊息、迭代、條件式(按這個順序)來表達控制流。只有在「不這樣做就會混淆主流程」時,才用 exception。

他對 Java 的 Checked Exception 更不客氣:宣告本身可以讓方法簽名長 50%、在丟和接之間每一層都多了要讀的東西、還讓重構變更難。這在當年是有爭議的立場(Java 社群分兩派),但歷史證明 Kent Beck 是對的——後來幾乎所有新語言(C#、Kotlin、Scala)都放棄了 checked exception。

最後一個實用點是 Exception Propagation:低層例外(NullPointerException 加一串堆疊)對終端使用者毫無意義,別把它直接噴到使用者臉上。把低層例外包裝進高層例外,給使用者看得懂的訊息,同時在 log 裡保留足夠診斷的細節。

注意事項

⚠️ 「用多型訊息取代條件式」是物件導向的招牌動作,但別變成宗教。Kent Beck 自己就提醒「choosing message 何時是多餘的」。現實裡,把一個簡單的三分支 switch 硬拆成三個類別 + 多型,常常是過度設計——讀者要跳三個檔案才看懂一件本來一眼就懂的事。現代語言的 sealed class + pattern matching(Java 21、Kotlin、Rust 的 match)其實提供了第三條路:既集中又型別安全又好擴充。所以今天的選擇不再是「醜 switch vs 多型」二選一,而是三選一甚至更多。

⚠️ Checked exception 的爭論在 Java 世界已經塵埃落定(大家基本上都同意 Kent Beck,少用或不用),但「例外是設計洩漏、盡量別用例外做控制流」這個更大的主張要小心別過度。在某些語境——比如 Python 的 EAFP(請求原諒比請求許可容易)風格——用例外做控制流是地道慣用法,不是壞味道。Kent Beck 的觀點根植於 Java 的文化和效能特性,換到別的語言生態未必照搬。聽眾要分清楚:哪些是普世原則(主流程要清楚),哪些是 Java 在地慣例(少用 checked exception)。

⚠️ Guard Clause 雖好,但「提早 return」過度使用也會反咬。一個方法裡散落十個 return,而且每個 return 前還做了不同的清理動作,一樣難讀。Guard clause 的甜蜜點是「簡單的、局部的前置條件檢查」——閃掉就好,沒有複雜善後。一旦每個出口都要做事(關資源、回滾),那可能是這個方法做太多事的訊號,該拆了。

專家補充

💡 「為主流程優化、把例外推到背景」這個直覺,跟認知科學的「漸進揭露(progressive disclosure)」是同一回事。人腦的工作記憶很小,一次只能裝一個「腦容量」的細節(這是 Kent Beck 後面講方法時的原話)。主流程清楚,等於先給讀者一張地圖;邊角情況退到背景,等於把地圖上不重要的標記淡化。好程式碼的可讀性,本質上是在配合人腦的限制。

💡 Kent Beck 對 checked exception 的批評,其實是一場著名的歷史辯論的一方。另一邊有人(如 Bruce Eckel 也批 checked,但 Java 設計者 James Gosling 當年是支持的)。最終的「市場投票」很清楚:C# 從一開始就不要 checked exception,Kotlin 明確移除,連 Java 自己的新 API(如 streams、lambda)都在迴避它。這是少數「語言設計實驗有明確結論」的案例——可以當成「設計決策也會被歷史檢驗」的好例子。

💡 「單一出口」教條的破除,連結到一段有趣的歷史。「單一入口單一出口」源自 1960-70 年代結構化程式設計運動(Dijkstra 那一脈),在當時是對抗 goto 義大利麵程式碼的進步。但任何規則一旦變成教條、脫離了它原本要解決的問題,就會在新環境裡變成枷鎖。Kent Beck 破的不是「結構化程式設計」本身,而是「把當年的具體規則供奉成永恆真理」這個態度。這跟前面破「匈牙利命名法」是同一種思考:規則要回到它的『為什麼』,環境變了就重新評估。

討論問題

🎙️ 挑你程式裡一個巢狀很深的 if。試著用 guard clause 把前置條件一條條提早 return 出去——主流程是不是瞬間浮出水面了?你會不會其實一直被「單一出口」的潛意識教條綁著?

🎙️ 你最近一次用多型(或策略模式)取代 switch,事後回頭看:它真的需要那個彈性嗎?還是那個 switch 本來一眼就懂,你拆成三個類別反而讓人要跳三個檔案?

🎙️ Kent Beck 說例外是「設計洩漏」。看看你最常丟例外的那個方法——它的例外有沒有逼著上面每一層呼叫者都得知道、都得處理?如果把它改成回傳值或 guard clause,主流程會更清楚嗎?

🎙️ 你的系統有沒有把低層的 NullPointerException 直接噴給使用者或塞進 API 回應?如果有,該在哪一層把它包裝成「人看得懂、但又保留診斷資訊」的高層例外?

更大範圍關聯

  • 接《Refactoring》(Martin Fowler):Guard Clause 在 Fowler 那裡就是一條正式的重構手法「Replace Nested Conditional with Guard Clauses」;用訊息取代條件式則是「Replace Conditional with Polymorphism」。這集講「為什麼這樣比較好」,Refactoring 給「一步步怎麼安全改過去」的食譜。

  • 接《Clean Code》:Uncle Bob 對函式的主張(函式要小、一個函式只做一件事、別用 flag 參數)和這章的主流程哲學完全同調。Clean Code 講得更規範化,這本給你背後的「力」與取捨。

  • 接函數式與 Railway-Oriented Programming:函數式社群處理錯誤的方式(EitherResultOption 取代例外)其實是 Kent Beck「例外是設計洩漏」這個直覺的徹底版——乾脆讓錯誤變成一個明確的回傳值,在型別系統裡顯性化。Rust 的 Result? 運算子是這條路的代表。可以把這集當成「為什麼現代語言不愛例外」的前傳。

  • 接語言演化史:checked exception 的興衰、「單一出口」教條的破除——這集處處在展示「設計規則是有時代性的」。這個元觀點本身可以接到一個更大的討論:我們今天奉為圭臬的最佳實踐,哪些十年後也會被推翻?

錄製建議

目標約 20 分鐘,單人主講。建議節奏:

  • 0–2 分:承接。回顧上集「狀態」,點出這集是另一面——行為、控制流。一句話交代這章兩個明星:訊息取代條件式、guard clause 馴服巢狀。
  • 2–6 分:Main Flow。先立主軸——「為常被讀的部分優化,把邊角推到背景」。這是後面所有模式的指導原則,要講清楚這個價值判斷。
  • 6–11 分:Message / Choosing Message。用 input/process/output 的例子講「訊息多承認了一層:細節可能會變」,再帶到「用多型取代 case」和它的代價(要跳檔案)。誠實提醒「別為不可能的變化硬塞訊息」。
  • 11–16 分:Guard Clause(本集最實用、最有共鳴)。直接秀巢狀 vs guard 兩段對照,敲「if-else 是對等替代,guard 是一重一輕」,再用破除「單一出口教條」製造一個小高潮。
  • 16–20 分:Exception + 辯證 + 收尾。講「例外是設計洩漏」「checked exception 歷史已證明 Kent Beck 對」,辯證一下「Python EAFP 是反例、現代語言用 Result 取代例外」。預告下一集合併「方法 + 集合」——最微觀的尺度。

口氣提醒:這集有兩段程式碼對照(guard clause、訊息取代條件式),口頭描述程式碼很難,建議用「想像一個巢狀三層的 null 檢查」這種畫面感語言,而不是逐行念 code。