AASM - classの状態遷移をスマートに実装するためのgem (Ruby, Active Record, Sequel, Mongoid)

# AASM - Ruby state machines

ここを参考にしました。
https://github.com/aasm/aasm/blob/master/README.md
https://github.com/aasm/aasm/blob/master/CHANGELOG.md

※2014年に本記事を投稿してから、細々と継続的にアクセスがあるようです。AASMの仕様は投稿当時と最新版でもそれほど変わっていないようですが、ご利用にあたっては必ず上記GitHubのREADMEとCHANELOGの確認をお願いします。

## 概要
AASMはRubyのclassに有限オートマトンを追加するライブラリ。2014年10月現在でも精力的に開発が進められている。以下の内容は gem version ~~~3.4.0~~~ 4.9.0 に準拠する。
http://techblog.heartrails.com/2011/02/rails-aasm.html
↑のブログ記事でAASMを見かけたヒトも多いと思うが、現在ではメソッド名などの仕様が変更されているので注意。
さらにいうと、version 4.0.0 でDSLも変更になったので注意。

## 用途
オブジェクトの状態遷移管理は大事。それを簡単に実装できるgemがAASM。
http://www.slideshare.net/tricknotes/rails-possiblestory
↑にあるような、「レコードを論理削除したら、機能がつかえなくなっちゃいました〜」みたいなポカを、オブジェクトの状態・遷移・遷移条件を明確にすることで防げる。(オブジェクトの状態とRDBのレコードの状態は別物、という話題はさておき)

## インストール

### RubyGems.org から手動で###

```sh
% gem install aasm
```

### Bundler を使って###

```ruby
# Gemfile
gem 'aasm'
```

### 自分でビルドする ###

```sh
% rake build
% sudo gem install pkg/aasm-x.y.z.gem
```


### Generators ###

After installing Aasm you can run generator:

```sh
% rails generate aasm NAME [COLUMN_NAME]
```

NAME には任意のModel名を入力する。COLUMN_NAME は optional です(デフォルトは 'aasm_state')。
This will create a model (if one does not exist) and configure it with aasm block.
For Active record orm a migration file is added to add aasm state column to table.

# 使い方
##状態・イベント・遷移を定義する
　状態遷移を管理したい Class に AASM モジュールを include して **state（状態）** と **event（イベント）** と **transitions（遷移）**を定義するだけでよい。
　下記では、`Job` というクラスには `sleeping`、`running`、`cleaning`という3つの状態があると定義した。この状態を遷移させるイベントは同じく3つで `run`、`clean`、`sleep`だ。

```ruby
class Job
  include AASM

  aasm do
    state :sleeping, :initial => true
    state :running
    state :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

end
```

　この定義により `Job`クラスのインスタンスに以下のようなpublicメソッドが追加される。追加されるメソッドは3種類で、インスタンスの状態を確認するもの、イベントが実行可能か確認するもの、イベントを実行するものの3種類だ。

```ruby
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
```

**event（イベント）**が実行不可にも関わらず実行をした場合(上記の`job.run`の参照)、`AASM::InvalidTransition` が発生する。ただし下記のようにaasm メソッドの第一引数で`:whiny_transitions => false`することで、例外の代わりに`boolean`型の戻り値を受け取ることも出来るようになる。

```ruby
class Job
  ...
  aasm :whiny_transitions => false do
    ...
  end
end

job.running?  # => true
job.may_run?  # => false
job.run       # => false
```
　イベント実行時にブロックを渡す事が出来る。このブロックは**transitions（遷移）**が成功するときのみ実行される。

```ruby
  job.run do
    job.user.notify_job_ran # Will be called if job.may_run? is true
  end
```

### 遷移にコールバックを追加する
　**transitions（遷移）**にコールバックを定義する事が出来る。コールバックは「特定の状態に遷移した場合のみ」など、任意の基準を満たした場合に実行される。

```ruby
class Job
  include AASM

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

    after_all_transitions :log_status_change

    event :run, :after => :notify_somebody do
      before do
        log('Preparing to run')
      end

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

    event :sleep do
      after do
        ...
      end
      error do |e|
        ...
      end
      transitions :from => :running, :to => :sleeping
    end
  end

  def log_status_change
    puts "changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})"
  end

  def set_process(name)
    ...
  end

  def do_something
    ...
  end

  def notify_somebody(user)
    ...
  end

end

class LogRunTime
  def call
    log "Job was running for X seconds"
  end
end
```

　上記の場合、状態は `sleeping`と `running`の２つである。ここでは `running`状態から `sleeping`状態に遷移する前(`before_enter`)に`do_something` が実行される。また、`notify_somebody` は `run`(from sleeping to running) イベントの完了後に呼ばれる。

AASM will also initialize LogRunTime and run the call method for you after the transition from runnung to finished in the example above. You can pass arguments to the class by defining an initialize method on it, like this:

```rb
class LogRunTime
  # optional args parameter can be omitted, but if you define initialize
  # you must accept the model instance as the first parameter to it.
  def initialize(job, args = {})
    @job = job
  end

  def call
    log "Job was running for #{@job.run_time} seconds"
  end
end
```

　また、イベントにパラメーターを渡す事も出来る。下記の場合は `set_process` が呼ばれ引数 `:defragmentation` が渡される。


```ruby
  job = Job.new
  job.run(:running, :defragmentation)
```

　もしイベントの処理中にエラーが発生した場合、エラーはrescueされ `:error`コールバックが実行される。:error コールバックでは、処理をおこなうことも、例外を発生させることもできる。

　`:on_transition` コールバックの中では遷移元の状態(`aasm.from_state`)と遷移先の状態(`aasm.to_state`)両方にアクセスできる。

```ruby
  def set_process(name)
    logger.info "from #{aasm.from_state} to #{aasm.to_state}"
  end
```

また、コールバックの実行中に、`aasm.current_event`を実行することで、発火中のイベント名を取得することが出来る

```ruby
  # 上記のJob#do_somethingに追記
  #do_somethingはsleepイベントを実行する前に呼ばれる。
  def do_something
    puts "triggered #{aasm.current_event}"
  end
```

```ruby
  job = Job.new

  job.sleep # => triggered :sleep
  job.sleep! # => triggered :sleep!
```
###  遷移にガードを追加する
　特定の条件を満たした場合にのみ状態遷移を許可するようにしたいとき、遷移に「ガード」を定義する事が出来る。ガードは遷移の実行前に実行される。ガードがfalseを返した場合、遷移は拒否される。(`AASM::InvalidTransition` 例外が発生するか、 `false` を返す)

```ruby
class Job
  include AASM

  aasm do
    state :sleeping, :initial => true
    state :running
    state :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, :to => :sleeping, :guard => :cleaning_needed?
    end
  end

  def cleaning_needed?
    false
  end

end

job = Job.new
job.run
job.may_sleep?  # => false
job.sleep       # => raises AASM::InvalidTransition
```

下記のように複数のガードを定義する事も出来る。

```ruby
    def walked_the_dog?; ...; end

    event :sleep do
      transitions :from => :running,
       :to => :sleeping, 
       :guards => [:cleaning_needed?, :walked_the_dog?]
    end
```

すべての遷移にガードを定義したい場合は、下記のようにする。

```ruby
    event :sleep, 
     :guards => [:walked_the_dog?] do
      transitions 
       :from => :running,
       :to => :sleeping,
       :guards => [:cleaning_needed?]
      transitions
       :from => :cleaning,
       :to => :sleeping
    end
```
##O/Rマッパー連携
### ActiveRecord

　AASM はActiveRecord においてデータベースでのオブジェクト永続化を可能にしている。

```ruby
class Job < ActiveRecord::Base
  include AASM

  aasm do # default column: aasm_state
    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

end
```

　objectをauto-saveしたりしなかったりできる。

```ruby
job = Job.new
job.run   # not saved
job.run!  # saved
```

　saveするときには `Job` classなら`Job`クラスのvalidationが走るが、状態については下記のように記述する事でvalidationをスキップすることができる。

```ruby
class Job < ActiveRecord::Base
  include AASM

  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

end
```

また、Rails 4.1以上では`state` を[enum型](http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html)で記述することができる。

```ruby
class Job < ActiveRecord::Base
  include AASM

  enum state: {
    sleeping: 5,
    running: 99
  }

  aasm :column => :state, :enum => true do
    state :sleeping, :initial => true
    state :running
  end
end
```

`enum`を使用したい場合、`aasm`の引数として`:enum => true`を渡すこと。もしDBのカラムの型がIntegerの場合（Railsでenumを使用した場合、大抵そうであるはずだが）は、`:enum => true`は不要である、もし、何らかの問題があって enumを使用させたくない場合、`aasm`の引数で ```:enum => false```を渡せばよい。

### Sequel

AASM は [Sequel](http://sequel.jeremyevans.net/) に対応している。
ただし、Sequel 向けの機能は、automatic-scopesがまだ実装されていないなど、ActiveRecord むけの機能と比較してまだ不十分なところがある。

```ruby
class Job < Sequel::Model
  include AASM

  aasm do # default column: aasm_state
    ...
  end
end
```

### Mongoid

AASM は [Mongoid](https://github.com/mongoid/mongoid)を利用することでMongodb にも対応している。ただし、利用の場合は `Mongoid::Document` を AASMより前にincludeする必要がある。

```ruby
class Job
  include Mongoid::Document
  include AASM
  field :aasm_state
  aasm do
    ...
  end
end
```
## トランザクション

　AASMは バージョン *3.0.13* からActiveRecordのtransactionsに対応している。そのため、状態遷移の失敗などが発生した場合、データベースレコードへのすべての変更はロールバックされる。トランザクションがコミットされた後にだけなんらかの処理を実施したい場合は、`after_commit` コールバックを利用する。

```ruby
class Job < ActiveRecord::Base
  include AASM

  aasm do
    state :sleeping, :initial => true
    state :running, :after_commit => :notify_about_running_job

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

  def notify_about_running_job
    ...
  end
end
```

### AASMが作成するカラムの名称を変更する

AASMはデフォルトでは状態の管理に `aasm_state` というカラムを使う。カラム名称は `:column` で定義する事が出来る。

```ruby
class Job < ActiveRecord::Base
  include AASM

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

end
```

　注意すべきは、カラム名称がなんであろうと、マイグレーションへのカラムの追加を忘れない事。その際の型は `string`とすること。

```ruby
class AddJobState < ActiveRecord::Migration
  def self.up
    add_column :jobs, :aasm_state, :string
  end

  def self.down
    remove_column :jobs, :aasm_state
  end
end
```
##オートマチック・スコープ
　AASM はmodelの各 **state（状態）**と同名の scope メソッドを自動作成する。

```ruby
class Job < ActiveRecord::Base
  include AASM

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

  def sleeping
    "This method name is in already use"
  end
end
```

```ruby
class JobsController < ApplicationController
  def index
    @running_jobs = jobs.running
    @recent_cleaning_jobs = jobs.cleaning.where('created_at >=  ?', 3.days.ago)

    # @sleeping_jobs = jobs.sleeping   #=> "This method name is in already use"
  end
end
```

scopeが不要に場合は、下記のように記述する。

```ruby
class Job < ActiveRecord::Base
  include AASM

  aasm :create_scopes => false do
    state :sleeping, :initial => true
    state :running
    state :cleaning
  end
end
```
## インスペクション

　クラスにどのような状態、イベントが定義されているか調査するメソッドも用意されている。

```ruby
job = Job.new

job.aasm.states.map(&:name)
=> [:sleeping, :running, :cleaning]

job.aasm.states(:permissible => true).map(&:name)
=> [:running]
job.run
job.aasm.states(:permissible => true).map(&:name)
=> [:cleaning, :sleeping]

job.aasm.events
=> [:run, :clean, :sleep]
```