State Pattern 與 State Machine

vic
12 min readAug 29, 2019

--

Preface

在軟體開發中,許多 Model 會有 state 這個屬性,state 除了說明 Model 的狀態以外,還有可能改變 Model 的行為,例如 訂單 就會有 待付款、已付款、送貨中、已取貨 這幾個 state,然而狀態的切換與管理並不是一個簡單的任務,例如你必須確保 待付款 的狀態不能直接切換到 已取貨,已及 送貨中 的狀態 訂單不能被執行 取消 這個行為,而 State Pattern 就是一個幫助 軟體工程師 設計狀態管理流程的一種設計方法,而 State Machine 則是透過 State Pattern 所實做出來的程式。

Table of Contents

  • 為何需要 State Pattern
  • State Pattern 的實做與優缺點
  • 使用 Transition Table 實做 State Machine
  • 使用 AASM 結合 State Machine 與 ActiveRecord

Contents

一、為何需要 State Pattern

使用情境 : 當物件本身有不同的狀態,且其內部狀態會改變物件的行為。

舉例:電燈本身有 這兩個狀態,而 按開關 這個行為會依照其目前狀態產生不同的結果。

實做範例 (不使用 State Pattern):

class Light
def initialize
@state = 'off'
end

def press_btn
if @state == 'off'
print 'dark'
@state = 'on'
elsif @state == 'on'
print 'bright'
@state = 'on'
end
end
end

然而上述程式法的寫法有幾個缺點:

  • 違反 開放封閉 原則:如果當 Light 這個類別需要新增新功能時,必須修改其 內部 press_btn 這個行為,這違反了 封閉原則 (i.e 進行擴展時,不須修改既有的程式碼)。
def press_btn
if @state == 'off'
print 'weak bright'
@state = 'weak'
elsif @state == 'weak'
print 'strong bright'
@state = 'strong'
elsif @state == 'strong'
print 'dark'
@state = 'off'
end
end
  • 難以維護與閱讀:所有狀態管理的被封裝到 press_btn 這個函數中,這導致 press_btn 函數會變得龐大,且 條件分支 變多時 會提高閱讀的難度。

二、State Pattern 的實做與優缺點

解決方法 (State Pattern):將物件中的狀態獨立成的物件(i.e State Object),並透過 Context Object 來切換物件的狀態。

實做範例 ( 使用 State Pattern ):

# 首先先設計好 Context Object 的結構
class LightContext
attr_reader :state
def initialize)
@state = OffState.new(self)
end
def press_btn
@state.press_btn
end
def set_state(new_state)
@state = new_state.new(self)
end
end
# 獨立 State Object 並設計其結構
class State
def initialize(light)
@light = light
end
def press_btn
raise Error
end

def next_state
raise Error
end
end
# 實做不同 State 的物件class OffState < State
def press_btn
print 'weak'
next_state
end
def next_state
@light.set_state(WeakState)
end
end
class WeakState < State
def press_btn
print 'strong'
next_state
end
def next_state
@light.set_state(StrongState)
end
end
class StrongState < State
def press_btn
print 'off'
next_state
end
def next_state
@light.set_state(OffState)
end
end

State Pattern 帶來的好處:

  • 符合 開放封閉原則:LightContext 這個類別在擴展新功能時,完全不需要修改其內部程式,只需要 建立一個新的 State 物件,並修改 State 物件之間串連的模式即可。
# 新增新的狀態class SuperStrongState < State
def press_btn
print 'super strong'
next_state
end
def next_state
@light.set_state(OffState)
end
end
# 修改狀態之間的關係class StrongState < State
...

def next_state
@light.set_state(SuperStrongState)
end
end
  • 可維護性高:State 與 Context 分離後,開發者 若想新增 狀態只需要 增加 State 物件,不需要更改 Context 的程式,避免影響到其他 State ,此外擺脫了條件分支,提高程式的可讀性,也避免 Context 物件過於龐大。

然而 State Pattern 也有其缺點:

  • 難以觀察 State 之間的關係:使用 State Pattern 時,會建立許多分離的 State Object,分散的 State 架構同時也代表 State 串連的邏輯分散,因此我們很難在一個單一的地方看出整個狀態轉換的邏輯。

為了改善這個缺點,我們可以使用 Transition Table 來實做 State Machine。

三、使用 Transition Table 實做 State Machine

何謂 State Machine:State Machine 顧名思義是管理狀態切換的機器,其管理狀態的邏輯是透過給予機器不同的 input 機器會依造目前的 state 進行轉移並產生對應的 output。

我們可以使用狀態圖來說明 State Machine 如何管理狀態的切換

Light State Machine 狀態圖

當 Light 皆收到 press button 這個 input 時,state machine 就會依照此狀態圖以及目前的狀態發生改變,並在改變後產生對應的 output。

上述說明了 State Machine 的運作,而透過 State Pattern 即可實現 State Machine,然而真正要解決的問題是如何不違背 開放封閉 原則的情況下,讓 State 轉移的邏輯不被分離到各個 State Object。

此時就要提到 Transition Table:Transition Table 是將上述的狀態圖用 Table 的形式呈現。

燈泡的 Transition Table

使用 Transition Table 實做 State Machine,我們仍會將 State 從 Object 中獨立出來,只是會將 State 轉換成 Transition Table 並將 State 的轉換邏輯封裝在 Transition Table 物件中,此時轉換邏輯將不會分散到不同的 State Object。

實做過程:

# Transition Table 負責定義狀態切換的邏輯
class TransitionTable
class TransitionError < RuntimeError
def initialize(state, input)
super "No transition from state #{state.inspect} for input #{input.inspect}"
end
end
def initialize(transitions)
@transitions = transitions
end
def call(state, input)
@transition.fetch([state, input])
rescue KeyError
raise TransitionError.new(state, input)
end
end
# State Machine 負責管理狀態的切換
class StateMachine
def initialize(transition_table, init_state)
@transition_table = TransitionTable.new(transition_table)
@state = init_state
end
attr_reader :state

def send_input(input)
@state, output = @transition_table.call(@state ,input)
output
end
end
# LightContext 負責實做狀態切換的 Output
class LightContext
STATE_TRANSITIONS = {
#state, input, next_state, output
[:off, :button_press] => [:on, :bright],
[:on, :button_press] => [:off, :dark]
}
attr_reader :state_machine def initialize
@state_machine = StateMachine.new(STATE_TRANSITIONS, :off)
end

def state
state_machine.state
end
def handle_event(event)
action = @state_machine.send_input(event)
send(action) unless action.nil?
end
def bright
print 'bright'
end
def dark
print 'dark'
end
end

使用 Transition Table 去驅動 State Machine 仍符合開放封閉原則,LightContext 可以透過修改 transitions 邏輯擴展功能,且不用修改 handle_event 函數,缺點在於需要新增對應的 output 函數在 LightContext 中可能會導致 Context Object 變得過於龐大。

四、使用 AASM 結合 State Machine 與 ActiveRecord

AASM 是一個有許多功能的 State Machine 同時也是一個 Gem ( i.e 套件 ),AASM 也是 Transition Table 驅動的 State Machine,他提供 DSL 去撰寫 Transition Rule,以及 callbacks 功能來管理 event 的 lifecycle。

範例( 定義 Transition Rules )

class Light
include AASM
aasm do
state :off, initial: true
state :weak, :strong
event :press_button do
transition from: :off, to: :weak
transition from: :weak, to: :strong
transition from: :strong, to: :off
end
end
end

上述範例所定義的 transition table 為

若要在每個 transitions 中加入對應的 output,可以使用 callback 功能

class Light
include AASM
aasm do
state :off, initial: true
state :weak, :strong

event :press_button, after: :bright do
...
end
end
def bright
put "#{assm.current_state}"
end
end

上述程式的 transition table 為

如果想知道更多 callbacks 的使用可以到這裡

然而在實做中,狀態管理會跟資料庫中的資料有關,AASM 可以直接跟 ORM 結合透過 event 去修改 Table 中的 state field。

class Light < ActiveRecord::Base
include AASM
aasm do # default column: aasm_state
state :off, initial: true
state :on
event :button_press do
transition from: :off, to: :on
transition from: :on, to: :off
end
end
end
Light.new.button_press! # save record

使用 AASM 在沒特別指定欄位的情況下,AASM 會預設欄位為 assm_state,若要指定欄位且欄位為 enum type 可以使用下列語法

class Light < ActiveRecord::Base
include AASM

enum state: [:off, :on]

aasm column: :state, enum: true do
...
end
end

AASM 除了上述的用法以外,還有許多強大的功能,例如 Scope 可以 query 不同 state 的 record,用 Namespace 定義不同的 transition rules 等等,如果想更了解 AASM 的實做可以到這裡

--

--

vic
vic

Written by vic

經驗生於思考,思考生於行動。

No responses yet