第 6 / 11 部 待錄

彎曲或弄壞

《The Pragmatic Programmer 務實的程式設計師》Podcast 準備稿:彎曲或弄壞

書名: 務實的程式設計師 The Pragmatic Programmer(Your Journey to Mastery, 20th Anniversary Edition) 作者: David Thomas & Andrew Hunt 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|The Pragmatic Programmer.解耦、轉換、繼承稅:寫能彎而不折的程式碼 (6/11) 涵蓋範圍: 第5章 彎曲或弄壞(Topic 28-32:去耦合、行走江湖、轉換式程式設計、繼承稅、設定)

背景速覽

第五章把第二章「可逆性」的「為什麼」變成「怎麼做」。開場一句話:生活不會靜止,我們寫的程式碼也不行。為了跟上瘋狂的變化步伐,我們要盡一切努力寫出盡可能鬆散、靈活的程式碼,否則它會迅速過時、脆到無法修復、在邁向未來的狂奔中被拋下。設計橋梁你要它剛性,設計軟體你要它靈活——這章就是教你怎麼做出能「彎曲」而不「弄壞」的決策:解耦、用事件響應、用轉換思考、別亂用繼承、把會變的設定推到程式外面。

一句話重點

讓程式碼能彎而不折的唯一辦法就是降低耦合——用「命令別詢問」避免火車殘骸、用事件與轉換管線取代糾纏的控制流、用介面/委派/mixin 取代繼承這種最重的耦合、把所有會變的值推到外部設定——因為耦合是變更的敵人,它把「必須一起改的東西」綁死在一起。

值得討論的重點

1. 去耦合:耦合是變更的敵人,別搭火車殘骸

Tip 44「Decoupled Code Is Easier to Change」。耦合是變更的敵人,因為它把必須同時改的東西綁在一起,而且耦合會傳遞——A 耦合 B、C,B 又耦合 M、N,A 實際上就耦合了一整串。最生動的反模式是火車殘骸(train wreck)customer.orders.find(id).getTotals().grandTotal = ... 這種一路鏈下去的呼叫,穿越五層抽象,頂層程式碼被迫知道 customer 有 orders、orders 有 find、order 有 totals……這些隱含知識全都不能再改。解法是 Tip 45「Tell, Don’t Ask」——別把物件內部狀態挖出來自己做決定再塞回去(那完全破壞封裝),改成把責任委派給該物件:customer.findOrder(id).applyDiscount(discount)。配套還有 Tip 46「別鏈式呼叫」(迪米特法則)、Tip 47「避免全域資料」、Tip 48「真的重要到要全域,就用 API 包起來」。

2. 行走江湖:用事件響應真實世界的混亂

Tip(章內)談 Topic 29——真實世界是混亂的、事情不斷發生、人會改變主意。作者主張寫響應式應用,核心概念是事件(資訊可用性的通知)。給了四種處理事件的策略:有限狀態機(FSM)——一組狀態 + 每個狀態下哪些事件會轉到哪個新狀態,最美的是它可以純粹表達成資料(一張 transition 表),不是一堆 if-else;觀察者模式——但要小心它造成的耦合與效能問題;發佈/訂閱(pub/sub)——發布者與訂閱者完全不認識彼此,耦合更低;響應式編程與串流——把事件當成可以像集合一樣 map/filter/組合的資料流(RxJS、Reactive Streams 一脈)。重點是:用事件解耦「誰觸發」與「誰反應」。

3. 轉換式編程:把程式想成一條資料管線

Tip 49「Programming Is About Code, But Programs Are About Data」。我們設計時老在想類別、模組、框架,卻很少想「轉換」。但所有程式都在把輸入轉成輸出。回到這個視角,許多煩惱會消失:結構更清晰、錯誤處理更一致、耦合大幅降低。最早的範例就是 Unix 管線——find . -type f | xargs wc -l | sort -n | tail | head 是一連串轉換,像工業生產線,一端進原料、一端出成品。找轉換的方法是 top-down:從需求定義輸入輸出,那就是代表整個程式的函式,再把它拆成一步步小轉換(transformations all the way down)。Elixir 的管線運算子 |> 把這個思路變成語法。配 Tip 50「別囤積狀態,傳遞它」——把資料在管線間傳遞,而不是讓物件各自藏一坨可變狀態。

4. 繼承稅:你想要香蕉,得到的卻是猩猩抱著香蕉和整座叢林

Tip 51「Don’t Pay Inheritance Tax」——整章最有觀點的一節。開場直接喊:你在用繼承嗎?停下來吧。 引 Joe Armstrong 名言點出問題本質。人們用繼承的兩個動機——「不想打字」(共享 code)或「喜歡型別」(建型別階層)——兩種都有毒。繼承就是耦合:子類耦合父類及所有祖先,連使用子類的 code 也耦合到全部祖先(父類把 move_at 改名,八竿子打不著的 Car 也會無聲壞掉);型別階層則會長成龐大怪物,一層層只為表達最微小的差異,變更會在層間波動。作者給三個替代方案,宣稱有了它們你永遠不需要繼承:介面/協定(Tip 52,把行為當型別用、表達多型)、委派(Tip 53,Has-A 勝過 Is-A,只暴露你要的方法,不把整個框架 API 漏給客戶)、Mixin/Trait(Tip 54,細粒度地共享功能)。

5. 設定:把會變的值推到程式碼之外

Tip 55「Parameterize Your App Using External Configuration」。任何「會變、且能在主程式碼之外表達」的值——外部服務憑證、日誌等級、port/IP、稅率、格式化細節、授權金鑰——都該放進外部設定,讓同一份 code 適應不同環境與客戶。格式優先用現成純文字(YAML、JSON),會被客戶改的結構化資料(如稅率)放資料庫表。但作者有個關鍵堅持:別把設定讀成全域變數,要包在一層薄薄的 API 後面,把 code 從「設定的表示細節」解耦——甚至做成「設定即服務(configuration-as-a-service)」,這樣設定可以在執行期更新而不必重啟。

注意事項

⚠️ 繼承稅這節最容易被聽眾聽成「繼承是邪惡的、永遠不能用」。請保留作者的語氣但補上脈絡:他反的是「為了共享 code 或建龐大型別階層而濫用繼承」,不是說繼承這個語言特性本身一無是處。真正的訊息是「composition over inheritance」這條老智慧——優先用組合/委派/介面,繼承留給真正的 is-a 且穩定的關係。

⚠️ 「Tell, Don’t Ask」與迪米特法則別講成「任何鏈式呼叫都有罪」。流暢介面(fluent API,像 builder、stream pipeline)那種刻意設計的鏈式呼叫不是火車殘骸——差別在於你是否「穿越了多層抽象去挖別人的內部結構」。對純資料轉換管線(如 stream)的鏈式呼叫,作者自己在 Topic 30 還大力推崇。把這條界線講清楚。

專家補充

💡 「香蕉、猩猩、叢林」是 Erlang 之父 Joe Armstrong 對 OO 繼承的著名吐槽:你只想要香蕉(一個方法),結果繼承讓你扛來猩猩(整個父類)抱著香蕉和整座叢林(所有祖先與依賴)。這句話濃縮了「繼承 = 過度耦合」的全部,是這集最好用的金句。

💡 「composition over inheritance」並非本書原創——GoF《Design Patterns》(1994) 就把它列為設計原則之一(“Favor object composition over class inheritance”)。本書的貢獻是把它講得更激進、更可落地(直接給你介面/委派/mixin 三條出路),並用「繼承稅」這個經濟學隱喻讓人記住:繼承不是免費的,你在預支未來的修改成本。

💡 「把程式當成資料轉換」這條,是函數式編程思想滲入主流的縮影。Unix 管線(1970s)→ MapReduce → Elixir/Clojure 的 pipeline → 現代 stream API / 資料工程的 ETL,是同一條河。本書 1999 初版就有 Unix 管線,二十週年版用 Elixir |> 重講,正好示範了這個思想的延續。可接到《A Philosophy of Software Design》與函數式社群的「資料優先」立場。

討論問題

🎙️ 開場鉤子:「設計一座橋,你要它越剛硬越好;設計一個軟體系統,你要的剛好相反——你要它能彎。因為橋不會被要求下個月改成隧道,但你的程式碼會。這集就是教你怎麼寫『彎得下去』的 code。」

🎙️ 自問自答:「繼承這麼經典的東西,作者憑什麼叫我『停下來』?」——因為繼承是最重的一種耦合:你不只綁了父類,還綁了它的所有祖先,連用你子類的人都被連坐。當你只想共享一個方法,繼承卻讓你扛來整座叢林。Has-A(委派)幾乎總是比 Is-A(繼承)更靈活。

🎙️ 帶走的一題:「在你的 codebase 搜一個 a.b().c().d() 這種長鏈呼叫——問自己:頂層這段 code 被迫知道了多少它本不該知道的內部結構?試著用『命令,不要詢問』把它拆掉。」

更大範圍關聯

  • 接續第二章可逆性:本章是 EP2「可逆性」的操作手冊——可逆性說「別把決定當最終決定」,本章說「用解耦讓決定可以被換掉」。兩集配著聽最完整。
  • 與三經典 + GoF:「Tell Don’t Ask」「composition over inheritance」「觀察者/pub-sub」直接對話 GoF《Design Patterns》;解耦與《Refactoring》的「搬移特性」「以委派取代繼承」手法一一對應——本書給原則,Fowler 給重構步驟。
  • 與《A Philosophy of Software Design》:Ousterhout 把複雜度定義為「依賴 + 模糊」,整章解耦處理的正是「依賴」;他的「深模組」也呼應委派/介面把實作藏在薄 API 後面。
  • 函數式 + 響應式脈絡:轉換式編程接 Unix 哲學與函數式社群;事件四策略(FSM、Observer、Pub/Sub、Reactive Streams)接前端的 Redux/RxJS、後端的事件驅動架構與 Kafka。

錄製建議

  • 建議時長:約 25-27 分鐘(五個 Topic、密度高)。配比:解耦 + 火車殘骸約 7 分(Tell-Don’t-Ask 是主菜)、行走江湖四策略約 5 分(快帶,講共同精神:用事件解耦觸發與反應)、轉換式編程約 5 分(Unix 管線好講)、繼承稅約 7 分(最有觀點、香蕉猩猩叢林是高潮)、設定約 3 分收尾。
  • 討論策略:用「降低耦合」這條主線把五節串成一氣——解耦是總綱,事件、轉換、反繼承、外部設定都是降耦合的不同手段。繼承稅是這集的爭議高潮,建議花時間把「反的是濫用、不是特性」講清楚,並用「香蕉猩猩叢林」當記憶點。
  • 結尾預告 EP7「並行」——解耦到極致、各部分能獨立運作之後,自然就走到並行:怎麼讓多段程式碼安全地同時跑?