状態遷移をスマートに実装するためのRailsプラグイン aasm

こんにちは。ハートレイルズの前島(@netwillnet)です。今回は aasm という、状態遷移をスマートに管理するためのRailsプラグインの紹介をしようと思います。

インストール方法

gem install aasm

だけ。

どういうときに使えるの?

複数の状態を持つモデルを管理するときに威力を発揮します。例として、所有している本の状態を管理するモデルを書いてみましょう。状態には

  • 未読(not_read)
  • 読んでる途中(reading)
  • 既読(read)
  • 捨てた(dump)

の4つがあるとし、 status カラムで管理します。また、別の状態として「友達に貸している」を管理する lending カラムを定義しているとします。

状態遷移系のメソッドを aasm を使わず、素直に書くと

class Book < ActiveRecord::Base
  # status  : 読書の状態を管理するカラム(integer)
  # lending : 友達に貸しているか否かを管理するカラム(boolean)

  before_create :set_initial_state

  STATUS = {:not_read => 0, :reading => 1, :read => 2, :dump => 3}

  def reading
    # 未読以外の状態/友達に貸している状態からは遷移させない
    if !status.nil? && status != STATUS[:not_read] || lending
      return
    end
    self.status = STATUS[:reading]
  end

  def read
    # 未読/読んでる途中以外の状態/友達に貸している状態からは遷移させない
    return if !status.nil? && status != STATUS[:not_read] &&  status != STATUS[:reading] || lending
    self.status = STATUS[:read]
  end

  def dump
    # 友達に貸している状態からは遷移させない
    return if lending
    self.status = STATUS[:dump]
  end

  private

  def set_initial_state
    self.status = STATUS[:not_read] if status.nil?
  end
end

こんな感じでしょうか。これくらいなら別に aasm を導入しなくても何とかなりますが、状態遷移させるかどうか判定する条件式が面倒ですね。もし、状態の数や条件がもっと多かったらどうでしょう・・・うんざりしてきますね。

これを aasm を使って書くと下記のようになります。

class Book < ActiveRecord::Base
  # status  : 読書の状態を管理するカラム(string)
  # lending : 友達に貸しているか否かを管理するカラム(boolean)

  include AASM

  aasm_column :status
  aasm_initial_state :not_read

  aasm_state :not_read
  aasm_state :reading
  aasm_state :read
  aasm_state :dump

  aasm_event :reading do
    transitions :to => :read, :from => :not_read, :guard => :not_lending?
  end

  aasm_event :read do
    transitions :to => :read, :from => [:not_read, :reading], :guard => :not_lending?
  end

  aasm_event :dump do
    transitions :to => :dump, :from => [:not_read, :reading, :read], :guard => :not_lending?
  end

  def not_lending?
    !lending
  end
end

どうでしょうか。だいぶすっきりしたように思いませんか?

それぞれのメソッドの意味

コードを見れば大体わかると思いますが、簡単に説明します。

aasm_column
aasm で扱うカラムを指定します。
aasm_initial_state
初期状態を symbol で指定します。
aasm_state
状態の候補を設定します。
aasm_event
引数に指定した symbol が、状態を変化させるためのメソッドになります。保存はされません。保存も同時にするには"!"をメソッド名末尾に付ける必要があります。 ブロック中の transition メソッドで 遷移先の状態(:toオプションで指定)と遷移前の状態(:fromオプションで指定)を設定します。:from で設定した以外での状態の時に遷移しようとすると、AASM::InvalidTransition の例外が発生し、遷移しません。また、:guard で指定したメソッドを実行して false が返ってきた場合も状態遷移しません(この場合は例外は発生せず、falseが返ります)。

まとめ

というわけで、 aasm を使うとこのような感じで複雑な状態管理をスマートに書くことが出来ます。みなさんも使ってみてはどうでしょうか。