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
コードの説明をざっくりと。
- 前提:モデルにはステータスを格納できる列が必要です。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
簡単に言えば、マップが適用できるようにするってところでしょうか。設定より規約に若干反しそうですが…
続きをかきました。
*1:コード内の英語はこんな感じです:quotation=見積、available=発行中、unavailable=失注、expiration=期限切、accepted=受注済ってことです。(間違ってたらごめんなさい、英語苦手です)
*2:stateという列名は変更可能。変更した場合は、この後の「acts_as_state_machine」内で指定できます。が、出来る限りstateという名前が良いでしょう。
*4:他にもあります。コード嫁
- 8 http://reader.livedoor.com/reader/
- 3 http://fastladder.com/reader/
- 2 http://b.hatena.ne.jp/add?mode=confirm&url=http://d.hatena.ne.jp/fujisan3776/20080907/1220793126
- 2 http://d.hatena.ne.jp/fujisan3776
- 2 http://www.google.com/reader/view/
- 1 http://209.85.175.104/search?q=cache:Egyw-g_QRGMJ:k.hatena.ne.jp/keywordblog/rm+iui.js&hl=ja&ct=clnk&cd=57&gl=jp
- 1 http://all.ind.sg/news/
- 1 http://b.hatena.ne.jp/add?mode=confirm&title=Rails%u3092%u696D%u52D9%u30B7%u30B9%u30C6%u30E0%u306B%u9069%u7528%u3059%u308B%u306A%u3089%u3001acts_as_state_machine%u306E%u5C0E%u5165%u3092%u691C%u8A0E%u3057%u307E%u3057%u3087%u3046 - %u5BCC%u58EB%u5C71%u3
- 1 http://b.hatena.ne.jp/entrylist?url=http://d.hatena.ne.jp/&sort=eid&of=350&threshold=3
- 1 http://b.hatena.ne.jp/onkn101/
