不少開發者喜歡刷題,特別是刷LeetCode,一方面,是因為許多公司在面試過程當中,會對求職者進行演算法之類的能力測試,另一方面,是因為有不少開發者認為,進行刷題不只是為了面試,同時也可以是平常用來訓練運算思維的一種工具。

雖然我沒有刷LeetCode的習慣,不過,一直以來,當我學習了一門新語言時,如果想更深入理解其特性,往往就會拿〈常見程式演算〉的一系列題目做練習,有時會玩玩語言特性,以便讓程式看來簡潔,有時會隨意重構,讓程式更清楚一些,有時基於物件導向或函數式的典範,而想著題目該怎麼寫。

在那些題目當中,當我每次拿〈康威生命遊戲〉來練習時,總是會讓我停下思考很久,題目的需求本身要解絕不難,只是每次遇上時我總會思考一遍:以物件導向的寫法來說,生命遊戲中的細胞到底該怎麼定義比較好?理想上,以細胞為單位會是個好主意,不過,總覺得職責上沒有分配得很好。

最近我在重新整理模式的文件,進度來到了狀態(State)模式,想著是否有比較不玩具的範例,可以適切地呈現出構成狀態模式前的考量,於是,我想到了生命遊戲的細胞,不就有生與死的狀態,若是從狀態管理來考量與重構,結果有可能是類似狀態模式的設計嗎?

State?還是Rule?

就像〈康威生命遊戲〉所談到的,遊戲可簡化為三個規則:「鄰居數為0、1、4、5、6、7、8時,細胞下次狀態必為死亡」、「鄰居數為2時,下次狀態不變」、「鄰居數為3時,下次狀態必為生存」。

運用結構化的寫法並不難,就是用if/else判斷哪個條件成立,然後做對應的狀態變更處理罷了,若要物件導向點的寫法,就是有個Cell實例,具有nextGen方法,將if/else判斷與細胞生死狀態變更的處理寫在裡頭,這沒什麼問題,畢竟生命遊戲最後只歸結為三個規則。

然而,有時候在更複雜的遊戲裡,可能會有更多的規則,並根據這些規則來針對各個角色進行對應的狀態轉移,此時,如果我們單純只用if/else之類的條件判斷來處理,結果就會出現瀑布化流程的判斷清單。

單就生命遊戲來說,想避免if/else流程,我們可以定義Rule類別,因為有三種規則,可以定義出三個Rule的次型態:Fatal(致命)、Stable(穩定)、Revivable(可復活),而這些Rule實例有個accept方法,接受Cell實例,根據各自封裝的狀態轉移方式,對Cell進行狀態變更的處理,若對完整的實現有興趣,我們可以參考〈State〉的說明。

如果就Gof原書說明所用到的類別圖來說,Cell會是Context的角色,而Rule會是State的角色。嗯?Rule是State?而就Gof書中提出的範例來說,用來封裝狀態轉移的角色,若命名為State,是沒什麼大問題,這是因為,書中舉了TCP連線作為範例,而TCPConnection本身含有State物件,也就是State本身就代表著TCPConnection的狀態,所以,狀態與切換規則正好是一對一對應。

不過,生命遊戲的細胞會有生與死兩個狀態,然而,遊戲簡化後的規則還是有三個,這時,我們看著Gof類別圖中的State角色,可能就會產生疑惑:State的次型態,應該是Dead、Alive嗎?如果是以這樣的方式設計,Dead、Alive的accept方法中,勢必要根據那三個規則,寫下不同的判斷式(而不是直接實現狀態轉移),這麼一來,朝此方向進行重構的意義不大啊!

就我的觀點來看,Gof類別圖中的State角色,若命名為Rule會更通用,因為可適用於狀態與切換規則不是一對一的情況,而且有時狀態切換,不見得會考慮Context角色的狀態,例如,生命遊戲簡化後的規則,只有考量鄰居數,細胞本身原本是生或死,不在考量之內。

提取規則用的狀態?

在要求不可變動(immutable)的場合,若要朝著狀態模式的方向實現,物件狀態無法改變下,替代方案會是生成新物件來封裝新狀態。

例如,在〈圖靈隨想〉我提到的Brainfuck等多種自動機,基本上,就是狀態機,因為,它們實現了狀態模式的概念,並採取了不可變動的特性。

事實上,現代一些前端程式庫或框架,為了處理前端複雜的規則與狀態,也經常隱含了狀態模式的實現,而為了便於管理狀態,也常會採用不可變動特性。

若以自動機的定義來說,「規則」包含了「狀態」,反過來說,狀態決定了該提取哪個規則,有時識別這種用來提取規則用的狀態很簡單,例如紅綠燈,目前紅接下來就是綠,綠接下來是黃,黃接下來是紅,紅綠燈的目前狀態就決定了要提取哪個規則。

有時,要去識別這個狀態不容易,就生命遊戲來說,提取規則用的狀態,其實是鄰居數量,而不是細胞本身的狀態,因為,套用規則才會改變細胞狀態。然而,最後其實也改變了整個盤面,也就是細胞鄰居數也被改變了,下一代就是根據新的鄰居數作為狀態,來取得對應的規則。

在實際的應用程式設計時,若規則與狀態的對應,因需求(增加)而(逐漸)構成瀑布化的流程,有時用來提取規則的狀態,可能與型態對應,而狀態轉移可能是某個流程。例如,你可能用了一串instanceof判斷物件型態,然後做出對應的動作,在物件導向程式語言的入門文件中,應該會解釋這麼做不明智,或許該考慮次型態多型,將各個動作定義在子類別。

有時用來提取規則的狀態,並不是那麼明顯,例如,生命遊戲的鄰居數為0、1、4、5、6、7、8,這乍看是一組規則,其實,鄰居數為0、1、4、5、6、7、8的狀態,都是用來提取同一個規則。而有時提取規則用的狀態,會是一組條件的組合,像是投資組合、保險條款組合等,你可能使用了and、or等邏輯運算,將數個條件組合出某個成立狀況,這時,很有可能就是隱含著這些條件組合,構成一個提取某規則的狀態。

因為有時提取規則用的狀態不是那麼明顯,開發者就常採用直覺而簡單的做法,也就是不斷地增加新的if/else(或switch),然後流程變成小瀑布,久而久之就構成了大瀑布……,你也許已親眼在程式碼中見過那壯闊的景象吧!

面對瀑布化的流程

狀態模式可能是解決瀑布化流程的一種思考方向,若情況更為簡單,例如,並不是要改變狀態,而是要執行某個動作,用物件封裝動作,以某值對應物件,這時,若是採用字典之類的結構,可以建立對應表,這可能也是個解決的方式。

因此,面對瀑布化的流程,你應該想到的是「可以這樣繼續下去嗎?」

如果那跟狀態有關,那狀態模式會是個思考的方向,當然,瀑布化的流程也有可能構成深入的巢狀層次,像是先前專欄〈避免隨意而重複的if/else〉談過的一些情況,此時,你是要繼續讓流程加深、加長呢?或是思考改進的方式呢?

專欄作者


熱門新聞

Advertisement