《實作模式》Podcast 準備稿:狀態的溝通
書名: 實作模式 Implementation Patterns 作者: Kent Beck 系列: 設計品味系列(程式碼層次的設計直覺) 公開標題建議: 設計品味系列|Implementation Patterns.第三集:狀態——什麼該存、什麼該算、放多近 (3/6) 涵蓋範圍: 第 6 章「狀態 State」(含 Access、Variable、Parameter、Constant、Initialization 等模式)
背景速覽
上一集講「類別」怎麼把邏輯和資料綁在一起。這一集放大看「資料」這一側——也就是狀態。狀態是程式設計最古老的隱喻(把世界看成不斷變化的東西),也是最麻煩的東西:並行性跟它處不來、重構工具因它變難寫。這一章談的全是「怎麼向讀者溝通你對狀態的決定」——存在哪、用什麼存、命名怎麼取、什麼時候初始化。
一句話重點
物件導向的本質是「一種應對狀態的策略」——把巨大的狀態切成一小塊一小塊互相隔離的迷你電腦;而管好狀態的關鍵口訣是:把相似的狀態放一起、把不同的狀態分開,判準是「同不同時用」和「同不同生命週期」。
值得討論的重點
重點一:物件導向是「對付狀態」的策略
Kent Beck 一開頭就給了一個很高的視角。狀態為什麼麻煩?因為只要你對某個狀態做假設,程式碼就有風險——你可能假設錯,或狀態在你背後被改了。函數式語言乾脆把可變狀態整個丟掉,但它們從沒真正普及——因為我們的大腦天生就被訓練來處理「變化的世界」,逼自己用無變數的方式思考,等於放棄太多有效的思維策略。
那物件導向怎麼辦?他的洞見是:物件導向語言本身就是一種應對狀態的策略。它把系統的狀態切成一塊一塊離散的小片,每塊嚴格限制別人對它的存取——這樣狀態就不會「在背後」被改。追蹤幾個位元組,比追蹤幾百萬個位元組容易太多了。每個物件就像一台有自己記憶體、跟其他小電腦隔離的迷你電腦。
但他也吐槽:現代語言(包括 Java)用 public 欄位模糊了物件之間的邊界。 物件間存取的方便,不值得拿物件的獨立性去換。這句話是後面所有 Access 模式的動機。
重點二:相似的狀態放一起——判準是「同時用」和「同生命週期」
這是整章的操作核心,口訣很簡單:把相似的狀態放一起,確保不同的狀態分開。 但「相似」怎麼定義?Kent Beck 給了兩個很實用的線索:
- 這兩塊狀態,是不是在同一個計算裡被一起用?
- 它們是不是同時產生、同時消亡(同生命週期)?
如果兩塊狀態既一起用、又同生命週期,那就該放近一點。這個判準會反覆出現在好幾個模式裡:
- Common State:所有同類物件都需要的狀態,就宣告成欄位(像
Point的x、y)。而且他強調——一個物件裡的 common state 應該都有相同的作用域和生命週期。如果你想加一個「只有部分方法用、或只在某方法執行時才有效」的欄位,那是個警訊,通常該把那塊資料挪去別的地方(參數、輔助物件)。 - Variable State:用 map 存「因實例而異」的狀態。靈活,但他明確說它最大的缺點是溝通爛——光看程式碼你根本不知道這物件到底需要哪些欄位才能運作。所以原則是:盡量用 common state,只有在「欄位會因使用方式而需要或不需要」時才用 variable state。
- Extrinsic State:某塊資料只對程式的一小部分(比如持久化機制)有意義,對其他人沒意義——那就別塞進物件,把它存在「用它的那一方」手上的 map 裡。否則會違反對稱性原則。
重點三:Access——直接存取 vs 間接存取,以及那條務實的預設線
該直接 this.x = 10 還是該透過 setX()?這是個吵了幾十年的問題,Kent Beck 給的答案很務實。
直接存取的優點是表達清晰——你讀到 x = 10 就確切知道發生什麼。缺點是喪失彈性——值一旦釘在變數裡,要改儲存方式就得改所有寫入它的地方。間接存取(accessor 方法)剛好相反:用清晰度換彈性,客戶端不再假設那個值是直接存的,所以你可以偷偷改儲存方式。
他的預設策略很清楚:
- 類別內部(含 inner class):允許直接存取。
- 外部客戶端:用間接存取。
這樣大多數存取都是清晰直接的,只有跨物件的邊界才付彈性的代價。他還補了一刀:如果一個物件的狀態大多是被外部存取的,那代表有更深的設計問題潛伏著——資料和操作它的邏輯沒放在一起。
更重要的是他對「規則」本身的態度。他說沒有適用所有人的直接存取通用規則——有人說只在 accessor 和建構子裡用、有人說只在單一類別內用。Kent Beck 的態度是:沒有萬用規則,程式設計師需要思考、溝通、學習,這正是專業的一部分。這句話我覺得是整本書的精神縮影。
重點四:Constant 與「用名字溝通意義」
Constant 看起來是最不起眼的模式,但 Kent Beck 講出了它真正的價值。很多人以為常數是為了「方便改值」——把散落的 5 集中成一個地方好改。這當然對,但他說最強的理由是溝通:你可以用常數的名字傳達那個值的含義。Color.WHITE 比 0xFFFFFF 好懂太多了。
他還點出一個隱藏的 bug 來源:如果 5 在你程式裡有兩個不同的隱含意義(一個是「畫邊框寬度」、一個是「確認封包編號」),當你要改其中一個,散落的字面常數讓你很容易改錯、或漏改。常數把「值」和「意義」綁起來,一整類錯誤就消失了。
這個模式其實是整章命名哲學的縮影。他在 Role-Suggesting Name(用角色命名變數)裡講得更透:不要把型別、作用域編進變數名(那是 C 時代的習慣,Java 有編譯器和 IDE 幫你了),把名字省下來專門傳達「這個變數在計算裡扮演什麼角色」。如果你想不出名字,通常是因為你還沒真正搞懂這個計算。
注意事項
⚠️ 「類別內直接存取、跨物件間接存取」這條預設線很實用,但別把它當教條。現代語言給了你更多選項——Kotlin 的 property、C# 的 property、record 的自動 accessor,都讓「直接寫法 + 未來可換實作」這件事不再互斥。Kent Beck 當年要在「清晰」和「彈性」之間二選一,現在很多語言讓你兩個都要。所以聽眾要帶走的是「跨邊界要保留換實作的自由」這個原則,而不是「一律手刻 getter/setter」這個 2007 年的 Java 動作。
⚠️ Kent Beck 對 getter/setter 其實是有戒心的——他說大量瑣碎的 getter/setter 會淹沒真正有用的方法、讓程式碼難讀,而且「物件狀態大多被外部存取」是設計病的徵兆。但現實中,很多框架(JPA、序列化、各種 ORM 和反射工具)逼你提供 getter/setter。所以這裡有個張力:Kent Beck 的理想(把邏輯搬到資料旁、別暴露狀態)和工程現實(框架要 bean 慣例)會打架。值得在節目裡誠實點出:他的方向是對的,但你得跟框架妥協。
⚠️ 這章有大量「用 map 存 variable state」「lazy initialization 省啟動時間」這類模式,背景是 2007 年的計算資源和 Eclipse 的啟動效能焦慮。今天的硬體下,lazy init 的效能理由弱很多,反而它帶來的「讀者要看兩個地方才懂這欄位」的可讀性成本更突出。Kent Beck 自己也說 lazy init 的欄位比 eager 難讀。所以:現在預設 eager,除非真有量測證據說 lazy 划算。 別為了想像中的效能,先付可讀性的帳。
專家補充
💡 「物件導向是一種應對狀態的策略」這句話,可以接到一個更大的圖景:過去二十年程式語言的演化,某種程度是「狀態控制權」的爭奪戰。物件導向用「封裝+隔離」對付狀態;函數式用「乾脆消滅可變狀態」對付狀態;Rust 用「所有權+借用檢查」對付狀態。它們是同一個問題的三種答案。Kent Beck 在 2007 年站在物件導向這一隊,但他對函數式的尊重(整章不斷提到「值的世界」)透露他早就看到趨勢。
💡 他講「Parameter 引入的耦合比永久參照弱」這段很有意思,而且舉了《格列佛遊記》的比喻:一個參數是連結兩個物件的細線,但夠多的細線(同一個參數傳五次)就能像小人國綁住格列佛一樣,讓物件動彈不得。這其實是「耦合」這個抽象概念一個極具畫面感的具象化。當你發現同一個東西反覆當參數傳,那是它「想變成欄位」的訊號——這跟後面 Parameter Object 模式是同一條線。
💡 「用名字傳達角色,而不是型別或作用域」這個主張,背後是一場歷史辯論。匈牙利命名法(strName、iCount)在 C 時代很流行,因為弱型別語言需要靠名字提醒型別。Kent Beck 主張在 Java 這種有型別檢查 + IDE 的環境,把這些資訊塞進名字是「以閱讀為代價優化打字」——而變數被讀的次數遠多於被寫。這個觀點今天已是主流,但值得提醒聽眾:命名的「正確答案」是隨工具環境變的,不是永恆的。
討論問題
🎙️ 挑你程式裡一個欄位多的類別。用 Kent Beck 的兩個判準檢查每一對欄位:它們「同時被用」嗎?「同生命週期」嗎?有沒有哪個欄位其實該降級成區域變數、或搬去輔助物件?
🎙️ 你的物件裡,狀態主要是被「自己的方法」用,還是被「外部」一直 get 來 get 去?如果是後者,Kent Beck 說那是設計病的徵兆——那塊邏輯是不是放錯地方了?
🎙️ 你寫 lazy initialization,是因為真的量測到啟動慢,還是只是「感覺這樣比較省」?如果是後者,你願不願意為了可讀性,先改回 eager?
🎙️ 看看你最近寫的一個變數名。它傳達的是「角色」(它在計算裡幹嘛),還是「型別/作用域」(它是什麼、住哪)?如果是後者,有沒有更好的名字?
更大範圍關聯
-
接函數式編程與《Out of the Tar Pit》:那篇經典論文主張「狀態是複雜度的最大來源」,跟 Kent Beck「狀態最麻煩」的判斷完全一致,但解法更激進(盡量消滅狀態)。這集講「怎麼封裝、隔離、溝通狀態」,函數式講「怎麼根本不要狀態」——兩條路的對照很值得辯。
-
接《Clean Code》:Uncle Bob 對 getter/setter 和「資料結構 vs 物件」的區分(Data/Object Anti-Symmetry)和這章的 Access 哲學高度重疊。兩人都警告「把物件當資料包」會丟掉物件導向最大的好處。
-
接《Effective Java》:本章的 Constant、Initialization、Parameter Object 在 Bloch 那裡都有對應條款(用 enum 取代 int 常數、builder 模式取代長參數)。Kent Beck 給直覺,Bloch 給更精煉的現代 Java 做法。
-
接 Rust 與所有權模型:Kent Beck 的「把狀態切成隔離的小塊、嚴格限制存取」在 Rust 的所有權和借用檢查裡被推到極致——編譯器強制執行「誰能改這塊狀態」。可以把這集當成「Rust 為什麼這樣設計」的一個直覺前傳。
錄製建議
目標約 20 分鐘,單人主講。建議節奏:
- 0–2 分:承接。回顧上集「類別綁邏輯和資料」,點出這集放大看資料那一側——狀態,也就是程式裡最古老又最麻煩的東西。
- 2–7 分:大視角開場。「物件導向是應對狀態的策略」這個高度的觀點,搭配「迷你電腦」的比喻。再帶出核心口訣「相似放一起、不同分開」和兩個判準(同時用、同生命週期)。這段是本集的骨架。
- 7–13 分:Access 主菜。直接 vs 間接的取捨,講透那條務實預設線(類別內直接、跨物件間接),特別敲「狀態大多被外部存取=設計病」這句。順帶帶出他對「沒有萬用規則」的態度——這是全書精神。
- 13–17 分:命名與常數。用 Constant「最強理由是溝通,不是方便改值」破題,延伸到「用角色命名、別編型別作用域進名字」。這段輕快,適合舉具體例子(
Color.WHITEvs0xFFFFFF)。 - 17–20 分:辯證 + 收尾。誠實面對「現代語言讓清晰和彈性不再二選一」「lazy init 的效能理由已弱、預設 eager」「getter/setter 要跟框架妥協」。預告下一集從「資料」轉到「行為」——控制流、訊息、例外。
口氣提醒:這章很容易講成「模式清單」。務必用「相似放一起、不同分開」這條主軸串所有模式,讓聽眾記住一個口訣勝過記住十個模式名。