第 4 / 10 部 待錄

差異爆發-狀態機與多型

《Functional Design》Podcast 準備稿:差異爆發・狀態機與多型

書名: Functional Design(Principles, Patterns, and Practices) 作者: Robert C. Martin(Uncle Bob) 系列: Reading Uncle Bob(恩普拉氏) 公開標題建議: Reading Uncle Bob|Functional Design.行為流經物件,還是物件流經行為 (4/10) 涵蓋範圍: 第8章 閒聊的巴士司機 + 第9章 物件導向程式設計(薪資系統)

背景速覽

上一集兩道簡單 kata 打成平手。這一集難度跳升,兩種風格的差異終於爆發。先是「閒聊的巴士司機」——一個真正的離散事件模擬、有限狀態機,而離散事件模擬正是 OO 的誕生主場(1966 年的 SIMULA 就是為它而生)。結果卻出人意料。接著是整本書最複雜的範例:薪資系統,它補上了前面缺的那一塊——多型,也就是 Uncle Bob 認為「OO 真正帶來好處的那項屬性」。

一句話重點

當程式的主軸從「計算」變成「逐刻變化的狀態」時,OO 和 FP 的分家就深刻起來:OO 把狀態藏進可變物件、依資料內聚切分;FP 把狀態外置成不可變資料、讓它流經一條函式管線、依行為內聚切分——一句話,OO 是「行為流經物件」,FP 是「物件流經行為」。

值得討論的重點

1. OO 的主場,FP 卻寫得更短更清楚

巴士司機這題是離散事件模擬——OO 的祖傳強項。Java 版按教科書 OO 設計:Driver、Route、Stop、Rumor、Simulation 各自封裝狀態,共 145 行、5 個檔案。Clojure 版沒有事先畫物件圖,測試直接驅動結構,結果 42 行、1 個檔案。更耐人尋味的是切分依據變了:Java 依「資料內聚」(每種資料一個物件),Clojure 依「行為內聚」(8 個函式裡 6 個只吃 world 一個參數)。Uncle Bob 說,Clojure 版的 Driver 根本不是 OO 意義下的物件——它沒有方法;唯一的「物件」是那個流經整條管線的 world,而且它還不可變。

2. 多型才是 OO 的精華——但 FP 也有,而且更靈活

薪資系統補上多型這塊。三類員工(月薪/時薪/抽成)、三種給付方式(郵寄/代收/銀行),這是 Strategy 模式的經典場景。Java 用繼承 + 多型介面實作。Clojure 用 defmulti/defmethod——但分派機制不同:Java 依「物件的內建型別」分派,Clojure 依「指定函式的回傳值」分派。這個差異看似小,威力卻大:Clojure 可以用一個 tuple(兩個自由度) 同時分派,這在多數 OO 語言難以做到。Uncle Bob 還展示,FP 能把「介面的不同方法實作在不同檔案」——Java/C# 根本做不到。

3. 兩種圖、兩種形狀:UML vs 資料流圖

一個很美的觀察:OO 用 UML 類別圖表達最自然,FP 用資料流圖(DFD)表達最自然。薪資的 FP 版本不是「迭代每個員工」,而是員工清單流經整支程式,每一站被略加修整——Uncle Bob 形容 FP 程式「更像水管系統,而非逐步流程」。但關鍵轉折在後面:透過 defmulti 寫在 payroll 之下、實作在執行期才註冊,FP 竟然能做出來源依賴反轉,畫出和 Java 幾乎一致的三層架構(高階政策 / 介面 / 低階實作)。形態不同,架構卻相通

注意事項

⚠️ 這集最容易被聽眾誤讀成「FP 完勝 OO」(42 行 vs 145 行多誘人)。要平衡:行數少不等於更好。Uncle Bob 特別警告 Clojure 的危險誘惑——它沒有 OO 那種「強制把函式分進 class」的紀律,函式容易被亂塞進 namespace,讓檔案結構變脆弱。他的建議反而是:寫 FP 時繼續沿用 OO 的切分紀律,不會吃虧。這句話很 Uncle Bob,要講出來。

⚠️ 「資料內聚 vs 行為內聚」是這集的核心對比,但抽象。一定要用巴士司機的具體例子落地:OO 把「司機相關的資料和方法」綁成 Driver 物件(資料內聚);FP 把「移動所有司機」做成一個函式、「散播謠言」做成另一個函式(行為內聚)。同一份資料穿過不同行為,而非不同資料各自封裝行為。

⚠️ defmulti 依「函式回傳值分派」這點很反直覺,但它是後面好幾集(SOLID、設計模式)的關鍵機制。這集要把它講清楚、講白話:「不是看你是什麼型別,而是看某個函式算出來的值,再決定用哪個實作。」

專家補充

💡 巴士司機這題的反轉特別有教育意義:OO 在自己的主場(離散事件模擬)被 FP 寫得更乾淨。但這不該被解讀成 OO 不行,而是揭示了一件事——當「狀態」是程式主軸時,「不可變 + 管線」這個組合在表達力上有真實優勢。SIMULA 當年用可變物件模擬,是因為那是 1966 年唯一的選擇;今天我們多了一個選項。

💡 薪資那段的「來源依賴反轉」是全書最精彩的技術橋段之一。defmulti 在上層、defmethod 在下層、實作在 runtime 向上註冊——這等於用動態分派做出了 Clean Architecture 的依賴規則。懂 Spring 依賴注入的聽眾會立刻有共鳴:這就是「控制反轉」的 FP 版,只是不需要 DI 框架,語言本身就給了。

💡 「物件流經行為 vs 行為流經物件」這句對仗,是理解整本書的鑰匙,值得反覆敲。OO 的心智模型是「我把訊息送給物件,物件改變自己」;FP 的心智模型是「資料像水流過一連串閘門,每個閘門產出新的資料」。這也解釋了為什麼 FP 天生親和 Unix pipe、Stream、LINQ——它們都是「資料流經行為」的具現。

討論問題

🎙️ 開場鉤子:「離散事件模擬,是物件導向 1966 年誕生的理由——這是 OO 的祖傳主場。如果在這個主場上,函式式版本反而寫得更短更清楚,你會怎麼想?今天這集,差異開始爆發。」

🎙️ 自問自答:「FP 沒有類別、沒有方法,那它怎麼做多型?」——靠 defmulti:不看你是什麼型別,而是看一個函式算出來的值來分派。這反而比 OO 的『綁死在型別上』更靈活,甚至能同時依兩個維度分派。

🎙️ 帶走的一題:「下次設計一個系統,先問自己一個問題:我的程式主軸是『計算』還是『狀態流動』?如果是後者,試著用『資料流經行為』的眼光重畫一遍——你可能會發現一個更清爽的結構。」

更大範圍關聯

  • 本書比較分析的高潮:這兩章是第二部差異最大的地方。下一集(型別 + 資料流)會把這個「物件流經行為」的觀察正式升級成設計原則。
  • 接續上集:上集說「題目越複雜差異越大」,這集就是兌現——巴士司機是前兩題的兩倍規模、且是真正的有限狀態機。
  • OO 的歷史源頭:SIMULA-67(Dahl & Nygaard,為模擬挪威海運而生)是 OO 與離散事件模擬的共同起點——這個冷知識讓「OO 主場被 FP 攻破」的張力更足。
  • 與《Domain Modeling Made Functional》對話:Scott Wlaschin 那本把「用型別和資料流建模業務」講到極致,正是這集「物件流經行為」的進階實戰,是給聽眾的絕佳延伸。用 [軟體工程領域切分藍圖] 看,扣到「領域建模 / 架構」這一支。

錄製建議

  • 建議時長:約 24 分鐘(兩章份量都重)。配比:巴士司機(狀態機、行數對比、資料 vs 行為內聚)約 11 分、薪資系統(多型、defmulti、來源依賴反轉)約 11 分。
  • 討論策略:巴士司機用「OO 主場卻被 FP 攻破」製造戲劇張力,再用「145 行 vs 42 行」「行為內聚 vs 資料內聚」點出本質。但務必接上 Uncle Bob 的平衡警告(FP 沒紀律會更亂)。薪資那段把 defmulti 依回傳值分派講清楚,再用「來源依賴反轉、架構卻相通」收尾——這是「FP 與 OO 不對立」最有力的一次論證。
  • 把「物件流經行為 vs 行為流經物件」這句金句放在這集尾巴,因為它正好是下一集(資料流 + SOLID)的引子。