狀態機初學到實作:認識 AASM

By Eileen Kuo・12月 31 2019・技術文章
狀態機初學到實作:認識 AASM

Source

https://github.com/aasm/aasm

說明

定義 state and event

首先需要有狀態,以及事件。事件為從 A 狀態轉變到 B 狀態。

aasm do
  state :sleeping, :initial => true
  state :running, :cleaning

  event :run do
    transitions :from => :sleeping, :to => :running
  end

  event :clean do
    transitions :from => :running, :to => :cleaning
  end

  event :sleep do
    transitions :from => [:running, :cleaning], :to => :sleeping
  end
end

然後

job = Job.new
job.sleeping? # => true
job.may_run?  # => true
job.run
job.running?  # => true
job.sleeping? # => false
job.may_run?  # => false
job.run       # => raises AASM::InvalidTransition

如果希望他不要跳其他訊息,只要 true false 就好的話可以用 :whiny_transitions => false

aasm :whiny_transitions => false do
  ...
end

那麼

job.may_run?  # => false
job.run       # => false(這裡就不會有錯誤訊息了)

也可以在 event 上加 block

job.run do
  job.user.notify_job_ran
end

這樣在 job.run 成功之後就會再執行 job.user.notify_job_ran

Callback

你可以為 transitions 設一些 callback,像是 after,下面這個範例在執行完 run 之後就可以去做 notify_somebody

class Job
  include AASM

  aasm do
    state :sleeping, :initial => true, :before_enter => :do_something
    state :running
    state :finished

    event :run, :after => :notify_somebody do
      transitions :from => :sleeping, :to => :running, :after => Proc.new {|*args| set_process(*args) }
      transitions :from => :running, :to => :finished, :after => LogRunTime
    end
  end

  def notify_somebody
    ...
  end
end

Transitions

一個 event 裡可以有不只一個 transitions,但是如果是從同一個狀態出發的第一個 transitions 成功之後,後面的 transitions 就不會再執行了

Guards

如果符合 guard 裡的條件才做這個 transitions,所以可以用來區隔同一個狀態根據不同過程會到不一樣的下一個狀態。

aasm_event :completes do
  transitions :from => sleeping, :to => :running, :guard => condition?
  transitions :from => sleeping, :to => :cleaning
end

def condition?
  some_contition
end

Bang events

  • 有無 ! 的差別:
job.run   # not saved,只是回傳告知是否可以做這個動作
job.run!  # saved
  • 不做驗證
aasm :skip_validation_on_save => true do
  state :sleeping, :initial => true
  state :running

  event :run do
    transitions :from => :sleeping, :to => :running
  end

  event :sleep do
    transitions :from => :running, :to => :sleeping
  end
end
  • 不能直接改變狀態
aasm :no_direct_assignment => true do
  state :sleeping, :initial => true
  state :running

  event :run do
    transitions :from => :sleeping, :to => :running
  end
end

那麼

job.aasm_state # => 'sleeping'
job.aasm_state = :running # => raises AASM::NoDirectAssignmentError
↑ 這裡無法直接將狀態改變
job.aasm_state # => 'sleeping'

Column name

AASM 預設用 aasm_state 去存 state,但是可以透過 override 的方式指定你想要用的 cloumn

class Job < ActiveRecord::Base
  include AASM

  aasm :column => 'my_state' do
    ...
  end
end

使用

說明

商城舉辦活動 (Activity),活動中顧客會成立訂單 (Order),而訂單中有商品 (Item) 這時就可以使用 AASM 來管理這三樣東西的狀態!

活動 (Activity) 有 active/inactive/pending 三種狀態。
訂單 (Order) 有 pending/inprogress/completed/deleted 四種狀態。
商品 (Item) 有 active/inactive/pending 三種狀態。

狀態情境:

  1. 如果活動開始,就要將 Activity 調成 active。
  2. 客戶必須在活動期間內才能成立訂單。
  3. 訂單中的商品必須是可以訂購 (active) 的狀態。
  4. 商品被訂購後須確認是否還有庫存,若無,要將狀態調為 inactive。

model/activity.rb

class Activity < ApplicationRecord
  include AASM
  aasm whiny_transitions: false, column: :state do
    state :pending, initial: true
    state :active
    state :inactive

    event :activate do
      transitions  :from => [:pending, :inactive], :to => :active
    end

    event :deactivate do
      transitions :from => :active, :to => :inactive
    end
  end
end

model/order.rb

class Order < ApplicationRecord
  include AASM
  aasm whiny_transitions: false, column: :state do
    state :pending, initial: true
    state :inprogress
    state :completed
    state :deleted

    event :placed, :after_commit => :deactivate_item do
      transitions  :from => :pending, :to => :inprogress do
        guard do
          activity.active? and item.active?
        end
      end
    end
  end

  def deactivate_item
    if item.count = 0
      item.deactivate!
    end
  end
end

model/item.rb

include AASM
aasm whiny_transitions: false, column: :state do
  state :pending, initial: true
  state :active
  state :inactive

  event :activate do
    transitions :from => [:pending, :inactive], :to => :active
  end

  event :deactivate do
    transitions :from => :active, :to => :inactive
  end
end

操作

$ activity.activate!

於是,活動就開始啦!我們的條件一也就完成啦!

接著,客戶建立了一筆訂單。那麼在 order.placed! 裡有

guard do
  activity.active? and item.active?
end

是要確保這個目前活動進行中 (也就是 Activity 狀態為 active ),以及客戶所訂購的商品有庫存 (也就是 Item 狀態為 active )。條件都符合時才能執行 order.placed!

並且,在 order.placed! 後有 after_commit ,所以如果成功執行 order.placed! ,將 Order 的狀態改為 inprogress 的話,就會執行 deactivate_item 這個 method。如果檢查確認 Item 數量已為 0,那麼就會 item.deactivate!

所以,條件二三四也都完成啦!

結語

介紹與簡易的 AASM 實作就這樣啦,有這些 transcation 就不用擔心在各個 model state會互相影響的複雜狀態下,調整了 A model 卻忘記調 B model 等等的狀況啦。


👩‍🏫 課務小幫手:

✨ 想掌握 Ruby on Rails 觀念和原理嗎?

我們有開設 🏓 Ruby on Rails 實戰課程 課程唷 ❤️️