Skip to content
April 20, 2013 / windperson

狀態模式(State Pattern)

狀態模式在GoF的Design Pattern這本書之中的定義如下:
“Allow an object to alter its behavior when its internal state changes. The object will appear to change its class”
這個的意思是:使某個物件可以在其內部狀態改變後,改變原本的行為模式,就像是變成了另一種類別的物件一樣。
UML類別圖
State Pattern
整個的結構可以分為:

  1. Context類別會提供至少一個的公開API方法(在這個圖上為Request()),client端呼叫該方法以便使得Context型態的物件做出應該要進行的行為動作。
  2. Context物件會有一個AbstractState型態的私有成員變數,來記錄Context物件它目前的狀態為哪種〝State〞,也就是該私有成員變數目前是哪一個AbstractState的子類別ConcreteStateN型態的物件。
  3. client端在呼叫Context物件的公開API方法以便做出預期應進行的行為動作時,有以下兩種設計架構:
    1. 原本GoF Design Pattern書中所敘述,實際進行行為動作的方法是寫在各個AbstractState的子類別之中,以此圖為例,就是那些ConcreteStateN的Handle()方法,client端在呼叫Context物件的公開API時,Context物件會「委派(delegate)」給那些ConcreteStateN類別物件的其中之一執行其類別中的Handle()方法。
    2. 實際進行行為動作的方法是寫在Context類別之中,而這些ConcreteStateN類別的用途僅僅是決定Context類別該使用哪套方法來滿足其公開API的呼叫,此種設計採用的情況通常是狀態種類不多,或是呼叫API後需要執行的方法需要額外讀取一些外部變數,或是每個狀態不需有『記憶性』:在事務邏輯上,當Context位於同樣狀態時,其用來表示狀態的狀態物件內容是和上次在該狀態時一樣。
  4. Context物件的State,會因為client端呼叫其公開方法Request()而產生的『內部觸發事件』,或是client端呼叫某些Context額外提供的其他公開方法而產生的『外部觸發事件』,而改變狀態,也就是改變其儲存的AbstractState子類別的物件。
  5. 當Context物件的State改變後,client再次呼叫Context的公開API方法,會得到和先前呼叫時,不同的回傳結果,或者會使得Context對另外不同的物件進行應有的行為操作。

這個狀態模式的UML類別圖和策略模式UML類別圖很像,實際上的不同在:

  1. 策略模式是在使用Context的client端,明確清楚知道總共有哪些〝策略〞可以採用。而狀態模式之下,使用Context的client端並不需要知道Context有那些〝狀態〞,只需要對於當前的Context物件,提供必要的資訊來促使Context改變狀態,再呼叫其公用方法Request(),即可達成任務。
  2. 在實作時,策略模式通常只需要套用其所提供的所有策略之中的一種策略來呼叫其方法即可完成,而狀態模式需要一整組AbstractState的子類別物件群和『如何改變狀態』的資訊,通常需要在開發程式前繪製Context的狀態圖(State Diagram)釐清整個可能的狀態轉移情形,方便在撰寫程式碼時參考以免寫錯。
  3. 策略模式是對於已知一件事情有幾種不同的〝方法〞可以得到結果的情形下,選取一種演算法來使用;而狀態模式是在目前是怎樣的狀態下,應該要做甚麼事情。
  4. 在重構程式碼時,如果目前處理事物是因一些判斷情況改變而有不同行為,但這些判斷情況不會有記憶性或累加性的,適合用策略模式來重構;相反的情況,使用狀態模式來重構較佳。

狀態模式的優缺點如下
優點:

  1. 將所有可能的不同行為方法呼叫,包裝成只需要呼叫Context類別的物件所提供的API方法即可達成,client使用該API時可減少撰寫要選擇採用哪個方法使用的額外邏輯判斷,降低client的程式碼複雜度。
  2. 對於在事務邏輯上沒有『記憶性』的狀態,用狀態模式可減少撰寫重複的程式碼。
  3. 可因應狀態(State)的不同,而在client呼叫Context提供的公用方法時,進行額外的動作。
  4. 在C#, Java這種有泛型(generic, http://en.wikipedia.org/wiki/Generic_programming )和映射(Reflection http://en.wikipedia.org/wiki/Reflection_(computer_programming) )機制的程式語言下,可在執行時期動態修改狀態模式的狀態類別之間狀態轉移的邏輯,進而能夠在程式執行時期動態增減狀態模式的行為動作。

  5. 將實際行為明確依照狀態的不同包裝至不同的類別程式碼內,減少日後修改某一個狀態內的行為時,導致全部的行為都有可能會出錯的機會。
  6. 日後有因應需求變化而需要新增,移除行為時,只要新增或移除State類別和修改對應的狀態轉移程式碼即可。(實際上這需要狀態模式的實作架構設計的夠好才行,所以這實際上是優點也是缺點)

缺點:

  1. 一個狀態一個AbstractState子類別(狀態類別)的設計架構會使得類別數量顯著增加,增加程式碼維護難度。
  2. 如果沒有整個狀態模式架構的狀態圖(state diagram),程式碼很容易日後看不懂或遺漏了某些狀態轉移狀況而出錯。
  3. 根據採用的實作架構不同,可能會有一些狀態類別之中有不少用來做狀態轉移的方法是不允許在該狀態之中執行該方法內容的,通常在實作上,會將該方法的內容留空白或是直接撰寫丟出例外的程式碼,造成很多乍看之下無意義的程式碼充斥在各個狀態類別,並且也使得這個狀態類別繼承結構,明顯違反了物件導向設計原則之中的里氏替換原則(Liskov Substitution Principle, LSP)。
    在實作上,可以藉由宣告一些額外『role interface』並且讓狀態物件實作這些interface,來避免充斥額外無意義程式碼的情形。
  4. 由於在執行時須保留很多狀態類別的物件在電腦記憶體內,增加額外的運算資源負擔,如果狀態模式之中的狀態轉移程式碼沒寫好,還有可能造成執行時發生記憶體洩漏的情形。
  5. 在採用狀態模式的程式碼可能會有多執行緒共同執行的情況時,須注意是否有因為想要減少前述缺點4.所描述的情形,而使用Singleton設計模式來管理狀態物件,造成每一個Context物件之間會有狀態錯亂的問題,如果有就不應該使用Singleton設計模式來管理狀態物件。

程式碼採用狀態模式撰寫時,在實際實作上,有幾個需要注意的地方:

  1. 建議一定要畫狀態圖,並且附在程式碼的文件之中,否則連自己日後都會看不懂,更別提別人在看code追蹤邏輯結構,或因需求變化而做程式內容修改時,絕對會搞不懂或是漏掉少寫一兩個狀態以及狀態轉移的程式碼而出錯。
  2. 進行單元測試(Unit test)時,一定要確定每一個state類別的方法和狀態轉移的邏輯路徑都有測試過,以免實際執行時因為某些狀態轉移邏輯路徑沒執行過測試過,而發生問題。
  3. 由於原本的狀態模式定義只有說,『讓一個物件有不同的狀態,而且其提供給外部的實質行為模式根據狀態的不同而有不同』,並沒有明確說明以下幾個必要的設計觀點:
    • 每個狀態的實質行為在何處實作。
    • 狀態與狀態之間的為何會發生轉移,以及轉移情形應如何實作。

    所以實際上採用狀態模式的程式,在程式碼架構上會有不少的變形。

Advertisements

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: