Hatena::ブログ(Diary)

富士山は世界遺産 このページをアンテナに追加 RSSフィード Twitter

2008-09-07

Railsを業務システムに適用するなら、acts_as_state_machineの導入を検討しましょう

acts_as_state_machineは、デザインパターンのステートマシンの実装です。

何が出来るか?などと論ずるより、まずインストールしてしまいましょう。script/plugin installで導入も数秒で完了します。

$ cd RAILS_ROOT
$ ruby script/plugin install http://elitists.textdriven.com/svn/plugins/acts_as_state_machine/trunk

言う事無し。

デザインパターン自体は知っていたのですが、実務、特に業務系システムへの適用とは無縁だと勝手に思っていました。しかし、acts_as_state_machineに出会ってから、それは大間違いだった事を知りました。

以下はどこにでもよくある、見積の状態遷移だと思います。

    +--> 失注
    |      ↑
    | +-> 期限切 -+
    | |           ↓
  発行中 ------> 受注済

図を見ると、やってはいけない(やれてはいけない)遷移が存在します。差替済→受注済や失注→差替済です。昔の私であればifやswtichで体当たり実装をしたことでしょう。

acts_as_state_machineは、状態遷移のような泥臭い実装を簡単に実現してくれます。

"受注済"に遷移するメソッドを、acts_as_state_machineで実装

論よりコード。

RAILS_ROOT/app/model/quotation.rb: (acts_as_state_machine版)
class Quotation < ActiveRecord::Base
  # POINT 1
  acts_as_state_machine :initial => :available

  # POINT 2
  state :available
  state :accepted, :enter => :accept_action
  state :expiration, :enter => :expire_action
  state :unavailable, :enter => Proc.new{puts "処理(をこうやって書く事も出来ます)"}

  # POINT 3
  event :accept do
    # POINT 4
    transitions :from => :available, :to => :accepted
    transitions :from => :expiration, :to => :accepted
  end

  event :expire do
    transitions :from => :available, :to => :expiration
  end

  event :loss do
    transitions :from => :available, :to => :unavailable
    transitions :from => :expiration, :to => :unavailable
  end

  private
  def accept_action
    puts "受注の処理"
  end

  def expire_action
  	puts "期限切れの処理"
  end

end

*1

コードの説明をざっくりと。

  • 前提:モデルにはステータスを格納できる列が必要です。acts_as_state_machineは「state」というstring型の列をステータス格納に利用します。*2
  • POINT 1 : acts_as_state_machineをこのモデルで使う事を宣言します。その際、このモデルの"ふりだし"ステータスを:initialに設定します。(ステータスはこの後stateで設定する中のものでなければいけません)
  • POINT 2 : stateでステータスを登録します。:enterは任意です。そのステータスに遷移したときに実行したいメソッド名を登録します。Proc.newでブロックを投げ込むことも可能です。
  • POINT 3 : eventでイベントを登録します。イベントとは遷移が発生するアクションの名前の事です。
  • POINT 4 : eventブロック内のtransitionsに遷移を記述します。遷移元が:from、遷移先が:toです。当該イベントが発生すると、ステータスが:fromであれば、:toに遷移します。transitionsは複数書く事が可能です。記述順にたどって、最初に:fromが一致したtransitionsの遷移が実行されます。一致しなければ無視されるだけです。*3

以上で完了です。動作を確認してみましょう。

# 見積を作成
a = Quotation.new(:quote_id => 1, total_price => 10000).save
=> true
e = Quotation.find(1)
=> #<Quotation id: 1, ... >
# 見積のステータスを確認
>> e.available?
=> true
>> e.current_state
=> :available
# 受注してみる
>> e.accept!
受注の処理
=> true
# ステータスを再度確認
>> e.available?
=> false
>> e.current_state
=> :accepted
# この状態でもういっちょ受注してみる
>> e.accept!
=> []
# 実は、accept! を実行時にsaveされている=saveし忘れがない。
>> Quotation.find(1)
=> #<Quotation id: 1, ..., state: "accepted" ... >

acts_as_state_machineを使うと、

  • Model#ステータス?
  • Model#イベント!
  • Model#current_state

というメソッドが実装されます*4。Model#ステータス?とModel#イベント!は、動的実装です。

ちなみに、これと同じものを"ベタ"に実装したらどうなるでしょうか?

"受注済"に遷移するメソッドを、ベタに実装してみる

論よりコード。

RAILS_ROOT/app/model/quotation.rb: (ベタ実装版)
class Quotation < ActiveRecord::Base
  STATE_AVAILABLE = 'available'
  STATE_EXPIRATION = 'expiration'
  STATE_ACCEPTED = 'accepted'
  STATE_UNAVAILABLE = 'unavailable'

  def before_save
    self.state = STATE_AVAILABLE if self.state.nil?
  end

  def current_state
    self.state
  end

  def available?
    self.state == STATE_AVAILABLE
  end

  def expiration?
    self.state == STATE_EXPIRATION 
  end

  def accepted?
    self.state == STATE_ACCEPTED
  end

  def unavailable?
    self.state == STATE_UNAVAILABLE
  end

  def accept!
    if (self.state == STATE_AVAILABLE) || (self.state == STATE_EXPIRATION) then
      self.state = STATE_ACCEPTED
      puts "受注の処理"
      return true
    else
      return []  # acts_as_state_machine版に合わせたため、ちょっと変かも。本来はraiseとかreturn falseかな
    end
  end

  def expire!
    if (self.state == STATE_AVAILABLE) then
      self.state = STATE_EXPIRATION
      puts '期限切れの処理'
    else
      return []
    end
  end

  def loss!
    if (self.state == STATE_AVAILABLE) || (self.state == STATE_EXPIRATION) then
      self.state = STATE_UNAVAILABLE
      puts '処理(をこうやって書く事も出来ます)'
    else
      return []
    end
  end

end

どっちが保守しやすいか?それが重要です

業務システムはビジネスに応じて変化しなければなりません。しかし…ステータスが増減したら?移行可能な条件が増減したら?

どっちのコードの方が保守しやすいでしょうか?私は、acts_as_state_machineでコードした方が、コード自体も仕様書の様ですし、良いと考えています。

ベタに実装した方は、遷移が細かくなればなるほどインデント数や条件が増えていき、テストも満足に出来ないコードができあがるでしょう。実際、私のところで動いている業務システムでは、8段程度のインデントは珍しくないです。解析だけで1日は潰せます。

しかし、こういった重要な部分ほど、お客さんの理解が得難いのも事実であり、時間がかけられないのです。だからこそ、パターンを適用・流用して楽をしたいものです。

acts_as_state_machineの弱点

ブクマコメントやコメントにももらったのですが、String#to_symもしくは:Stringってやれば良いんですね〜。

この辺を反映したコードを、今日あたり公開します〜。どもありがとうございます。<みなさま

acts_as_state_machineは万能ではありません。弱点もあります。

業務システムではステータスを数値文字('00' とか '99')で表現する事が多いです。しかしacts_as_state_machineはこれに対応できないのです。

class Model < ActiveRecord::Base
  acts_as_state_machine :initial => :00
  state :00
  #・・・・
end

こうなっちゃうんですよ。完璧な文法エラーです。ここがacts_as_state_machineの泣き所ではないでしょうか。

回避案はあるのですが、現在検証中です。コメントで煽ると、早く仕事するかもしれません (T_T

簡単に言えば、マップが適用できるようにするってところでしょうか。設定より規約に若干反しそうですが…

続きをかきました。

http://d.hatena.ne.jp/fujisan3776/20080907/1221480262

*1:コード内の英語はこんな感じです:quotation=見積、available=発行中、unavailable=失注、expiration=期限切、accepted=受注済ってことです。(間違ってたらごめんなさい、英語苦手です)

*2:stateという列名は変更可能。変更した場合は、この後の「acts_as_state_machine」内で指定できます。が、出来る限りstateという名前が良いでしょう。

*3:routerのフィルタに考え方が近いでしょう。

*4:他にもあります。コード嫁

fujisan3776fujisan3776 2008/09/13 02:48 自コメント。

なんと、
”00”.to_sym でいけるよ
というブクマコメントが!!!!!!!!

試してみます。

yuguiyugui 2008/09/14 08:52 :’00’ で同じ値を得られますよ。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証