《Design Patterns 設計模式(GoF)》Podcast 準備稿:行為型模式 III — State、Iterator 與 Memento
書名: 設計模式 Design Patterns(Elements of Reusable Object-Oriented Software) 作者: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides(Gang of Four) 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Design Patterns.狀態機、走訪器與時光快照:State/Iterator/Memento (10/11) 涵蓋範圍: State、Iterator(深講)+ Memento(速覽)
背景速覽
行為型模式第三棒。這集三個模式各管一件很具體的事:State(狀態)——讓物件「換了狀態就像換了類別」,把滿天飛的 if/switch 變成乾淨的狀態機;Iterator(迭代器)——「在不暴露內部結構的前提下逐一走訪元素」,這個模式現代化到幾乎每個語言都內建了;Memento(備忘錄)——「在不破壞封裝的前提下,存一份狀態快照供日後還原」,是 Undo 的另一塊拼圖。
這集有個有趣的觀察:Iterator 是 GoF 模式裡「最成功也最隱形」的一個——它成功到變成語言的 for-each、foreach、迭代協定,你天天用卻早已不覺得它是「模式」。這正是全季主題「模式被語言吸收」的巔峰案例。
一句話重點
State 把每個狀態的行為封裝成獨立物件、靠替換 state 物件來轉換,消滅大型條件判斷、讓狀態轉換顯式化;Iterator 把「走訪」從聚合物件抽離成獨立物件,讓你不暴露內部就能逐一存取;Memento 在不破壞封裝下捕捉並外部化物件狀態——三者各自解決「狀態、走訪、快照」這三個日常難題。
值得討論的重點
1. State:把狀態機從 if/switch 地獄裡解放出來
Intent:讓物件在內部狀態改變時改變行為,使它看起來「好像換了類別」。別名 Objects for States。
動機:一個 TCPConnection 可以處於 Established、Listening、Closed 等狀態,同一個請求(如 Open)在不同狀態下效果完全不同。天真做法是每個方法裡塞一堆 if (state == ...),散得到處都是、難維護。
State 的解法:定義抽象 TCPState,子類別 TCPEstablished、TCPClosed 各自實作該狀態下的行為。TCPConnection 持有一個 TCPState,把所有狀態相關請求委派給它。狀態轉換 = 換掉那個 state 物件(從 TCPEstablished 換成 TCPClosed)。
兩大價值:
- 狀態行為局部化:每個狀態的行為集中在自己的類別,新增狀態 = 加一個子類別,消滅散落各處的大型 if/switch。
- 狀態轉換顯式化:轉換變成「重新綁定一個變數」,而非偷偷改一堆內部旗標——這保護 Context 不會進入「半套」的不一致狀態。
進階點:若 state 物件沒有實例變數(行為完全由型別決定),多個 Context 可共享同一個 state 物件——此時 state 物件就是個沒有內在狀態的 Flyweight。(模式又組合了。)
要對照一句:State 和 Strategy 結構幾乎一樣(都委派給一個物件),差別在意圖——Strategy 的策略物件由 client「主動選」、彼此獨立;State 的狀態物件通常自己決定下一個狀態是誰、彼此有轉換關係。
2. Iterator:把走訪抽出來,現代語言的內建模式
Intent:提供一種方法,在不暴露聚合物件內部表示的前提下,循序存取其元素。別名 Cursor。
動機:一個 List 該能讓你走訪元素,但不該暴露它內部是陣列還是鏈結串列;你還可能想用不同順序走訪、或同時跑多個走訪。解法是把走訪責任從聚合物件抽到獨立的 Iterator 物件(First/Next/IsDone/CurrentItem),它自己記住「走到哪了」。
要點:
- 多型迭代:定義抽象
Iterator,聚合物件用 Factory Method(CreateIterator)建出對應的 iterator,於是 client 程式碼獨立於具體聚合類別。(Iterator + Factory Method 的經典組合。) - 支援同時多個走訪:每個 iterator 自帶狀態,同一個聚合可以同時跑好幾個走訪。
- External vs Internal iterator:external(client 自己呼叫
Next,控制權在 client)vs internal(把走訪邏輯交給 iterator,傳一個操作進去)。走訪 Composite 樹時,internal 可借遞迴呼叫堆疊隱式記路徑,常更簡潔。 - NullIterator:一個
IsDone永遠 true 的退化迭代器,讓樹走訪的葉節點處理統一。
3. Memento:不破壞封裝地存一份時光快照(速覽)
Intent:在不違反封裝的前提下,捕捉並外部化一個物件的內部狀態,使它日後可恢復。別名 Token。
動機:做 checkpoint/undo 要存物件狀態,但物件把狀態封裝在內部,公開它又會破壞封裝。Memento 的巧思是雙重介面:
- Originator(狀態的主人)看到 memento 的寬介面——能存取所有恢復用的資料。
- Caretaker(保管者,通常是 undo 機制)只看到窄介面——只能拿著、傳遞 memento,看不到裡面。
於是狀態被外部化保存,封裝卻沒被破壞(C++ 用 friend 實現這個不對稱可見性)。
代價(一句帶過):memento 可能很大、頻繁建立成本高;caretaker 不知道 memento 多大,無限 undo history 可能吃爆記憶體。增量 memento(只記「這次改了什麼」,常搭配 EP9 的 Command history)是降本關鍵。
注意事項
⚠️ State 和 Strategy 長得幾乎一樣,但別混為一談。 兩者都是「Context 委派給一個物件」。區別在意圖與動態:Strategy 的策略由外部選定、策略之間互不相干、通常不自己切換;State 的狀態物件彼此有轉換關係、常常自己決定下一個狀態,整體構成一台狀態機。問自己「這些物件之間有沒有『轉換成彼此』的關係」——有就是 State,沒有就是 Strategy。
⚠️ Iterator 你幾乎不該自己手寫了——但要懂它的概念。 現代語言全都內建迭代協定(Java 的 Iterable、C# 的 IEnumerable、Python 的 __iter__、JS 的 Symbol.iterator),for-each 背後就是 GoF 的 Iterator。所以這集講 Iterator,重點不是教你實作,而是讓你理解「為什麼 for-each 能跑、它在你和資料結構之間做了什麼」——以及極少數需要自訂走訪(如複雜樹結構)時,它的設計取捨。
專家補充
💡 Iterator 是 GoF 模式『被語言徹底吸收』的冠軍。 1994 年它還需要你定義一堆 Iterator 子類別、CreateIterator 工廠方法;今天它變成語言關鍵字 for...in、foreach、一個 yield。你寫 for (x in list) 從不覺得自己在用「設計模式」——但你就是在用。更進一步,lazy iterator / generator(yield)讓無限序列、串流處理變得可能,這是 GoF 當年沒料到的延伸。Iterator 完美演示了 EP1 的主題:成功的模式,最終會消融進語言本身。
💡 State 模式是『把複雜 if/switch 變成多型』的代表,但別什麼狀態都套它。 它的甜蜜點是「狀態多、每個狀態行為豐富、轉換複雜」的場景(協定、遊戲角色、訂單流程)。但如果你的狀態機很簡單,GoF 自己也說可以用 table-driven(一張轉換表)取代——State 模式聚焦「建模狀態特定的行為」,轉換表聚焦「定義狀態轉換」。狀態行為很薄、只是轉換的話,一張表比一堆類別更清爽。別為了「消滅 if」而換來一堆只有一行的 state 類別。
💡 Memento 是『把封裝和可還原性同時做到』的優雅解。 它解決一個真實張力:你想存物件狀態(為了 undo),又不想把內部全公開(破壞封裝)。雙重介面這招——同一個物件對不同對象露出不同的面——是很值得品味的設計。現代的應用:遊戲存檔、編輯器的 undo stack、資料庫的 savepoint、React 的時光旅行除錯(time-travel debugging)。它和 Command(EP9)是 Undo 機制的左右護法:Command 記「做了什麼動作」,Memento 記「當時的狀態長怎樣」。
討論問題
🎙️ 開場鉤子:「for (item in list)——這行你寫過幾萬次?你知道它背後是一個有正式名字的設計模式嗎?叫 Iterator。今天三個模式都很『日常』:管狀態的、管走訪的、管存檔的。其中 Iterator 已經被語言吃進去,變成你天天用卻不自知的關鍵字。」
🎙️ 自問自答:「State 和 Strategy 結構一樣,怎麼分?」——看那些物件之間有沒有『轉換成彼此』的關係。有(A 狀態會切到 B 狀態)就是 State,是台狀態機;沒有(純粹由外部選一個演算法)就是 Strategy。
🎙️ 帶走的一題:「找一段你寫過的、滿是 if (status == ...) 的程式碼。想想:如果每個狀態變成一個物件、轉換變成換物件,會不會更清楚?這就是 State 模式。但也誠實問自己——這值得嗎,還是一張轉換表就夠了?」
更大範圍關聯
- Iterator 與語言內建迭代/串流:
for-each、yield、generator、lazy stream(Java Stream、RxJS、LINQ)都是 Iterator 的演化,是「模式被語言吸收」最徹底的案例(呼應《Functional Design》對模式換形的觀點)。 - State 與狀態機/工作流:訂單流程、遊戲 AI、協定實作、前端的狀態機庫(XState)都是 State 模式的工業化;與 table-driven 的取捨是設計判斷的好教材。
- Memento + Command = Undo 雙護法:兩者合力支撐 undo/redo、time-travel debugging、Event Sourcing 的快照(呼應 EP9)。
- 與 Refactoring 的接點:「Replace Conditional with Polymorphism」「Replace Type Code with State/Strategy」等重構手法直接通往 State(見 [軟體工程領域切分藍圖])。
錄製建議
- 建議時長:約 23-25 分鐘。配比:State 約 9 分(含 if/switch 解放、vs Strategy、vs table-driven)、Iterator 約 8 分(重點放「被語言吸收」而非實作)、Memento 速覽約 5 分(含 + Command 的 Undo 雙護法)。
- 討論策略:Iterator 用
for-each開場最接地,且明確告訴聽眾「這集不是教你寫 iterator,是讓你懂 for-each 的底層」。State 段把「if/switch 地獄 → 狀態機」講成記憶點,並務必處理「vs Strategy」「vs 轉換表」兩個辨析。Memento 點到為止,重點扣回 EP9 的 Command 組成「Undo 雙護法」。 - 全季「模式被語言吸收」的主題,在 Iterator 這裡到達高潮,值得明講一次。