Rails で使える StateMachine Gem 3 つをしらべてみた

Rails 上で ActiveRecord を使って status を保存できる statemachine 系の gem として、だいたい aasm で作ってたので twitter でなんかないかなぁと書いたところ、以下の返答をもらった。
 
 <blockquote class="twitter-tweet" data-lang="en"><p lang="und" dir="ltr"><a href="https://twitter.com/takkanm">@takkanm</a> <a href="https://t.co/QhJoX1CKAT">https://t.co/QhJoX1CKAT</a></p>&mdash; 80.0 (@flada_auxv) <a href="https://twitter.com/flada_auxv/status/707095241975672833">March 8, 2016</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
 
<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr"><a href="https://twitter.com/takkanm">@takkanm</a> <a href="https://twitter.com/flada_auxv">@flada_auxv</a> 更に追撃w <a href="https://t.co/v9W3Mam3el">https://t.co/v9W3Mam3el</a></p>&mdash; joker1007に宜しく (@joker1007) <a href="https://twitter.com/joker1007/status/707096406494806017">March 8, 2016</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>

実はこの返答を得る前に aasm をいれちゃってたんだけど、今回はスパイク中のコードで使ったので、本実装時に再度選ぶことになるだろうから、aasm  と教えてもらったものを簡単にまとめておく。

## TL;DR;

どれを選ぶかは要件次第なので、これが絶対にお勧めということを書く気はないんだけど、私だったら以下のような選択するかなぁ。

- ActiveRecord の enum を利用したいなら [aasm](https://github.com/aasm/aasm)
- 状態の遷移をトラッキングしたい履歴が重要になるなら [statesman](https://github.com/gocardless/statesman)
- 状態が遷移した時間は必要になりそうなら [transitions](https://github.com/troessner/transitions)

## [aasm](https://github.com/aasm/aasm)

GitHub での star は、この 3 つの中では最多の 2,236(2016/03/08 19:00 現在)。
3 つの中で唯一過去プロジェクトで使ったことがあるやつ。

state 管理したいクラスに `AASM` を include し、`aasm do ... end` というブロックに state 管理のためのコードをまとめる。
ActiveRecord との連携では、デフォルトでは `aasm_state` というカラムを利用することになるが、Rails の enum カラムを利用しても state 管理ができる。

対象の ActiveRecord クラスには、`クラス.state_name` のような scope と、オブジェクトには `オブジェクト.state_name?` で期待する state であるかというメソッドと `オブジェクト.may_state_name?` という、指定した state に遷移できるかを確認するメソッドができる。

transition は `event :event_name do ... end` というブロックに書いていき、`object.event_name` で遷移できるようになる。`object.event_name!` で保存までやってくれる。

event や transition 単位に、`before` や `after` 、 `error` のコールバックを記述しておくことができる。他にも[コールバックとして差し込めるポイント](https://github.com/aasm/aasm#callbacks) は複数用意されている。

また、各 transition ごとに guard 条件を設定できるので、state の状態以外で遷移できるかの可否を記述することもできる。

以下、サンプルコード。

```
class Item < ActiveRecord::Base
  include AASM
  
  enum state: {wip: 0, ship: 10, archived: 100}
  
  aasm column: :state, enum: :true do
    state :wip, initial: true
    state :ship
    state :archive
    
    event :publish do
      before do
        self.published_at = Time.current
      end
      transitions from: :wip, to: :ship
    end
    
    event :archive do
      transitions from: :wip,  to :archive
      transitions from: :ship, to :archive
    end
  end
end
```

## [statesman](https://github.com/gocardless/statesman)

こちらの GitHub の star 数は、694(2016/03/08 19:00 現在)。

stateman は、他の 2 つと違い StateMachine 用のクラスが必要になるのが特徴。
statemachine を導入すると最初は少なかった状態数がプロジェクトが進むにつれ徐々に爆発していくことがあるので、その点ではスッキリ書けそう。

`state` や `transition`、`guard_transition`, `before_transition` などで、状態管理のための処理を宣言的に書ける。

状態を保存するには ActiveReocrd で、StateMachine を保存する用の table を用意したり、State 管理したい対象に関連をもたせてあげたりと[いろいろしてあげないといけないことが多い](https://github.com/gocardless/statesman#persistence)。以下の migrate 生成コマンドを実行する必要あり。

```
$ rails g statesman:active_record_transition 状態管理をしたいクラス名 状態保存用クラス名
```

しかし、各 state をカラムじゃなくレコードとして持つことができるので、状態遷移の履歴を後から参照できる。

状態を確認したいときや、遷移させたいときは、aasm のようにはいかず、`current_state` や `can_transition_to?(state名)` 、`transition_to(state名)` のように指定しないといけない。Scope も `in_state(state名)` のようになる。

## [transitions](https://github.com/troessner/transitions)

こちらの GitHub の star 数は、464(2016/03/08 19:00 現在)。

この gem は aasm のように、`state_machine do ... end` で state 管理したいクラスに情報を記述していくスタイル。
`state` で 状態を記述し、`event :event_name do ... end` でイベントを書いて、`transitions` でトランジションを書くのも似てる。
[コールバックのタイミングや種類](https://github.com/troessner/transitions#callback-overview)は独特な感じで、state に対しての `enter` や `exit` に対してもコールバックがセットできるのが他の 2 に比べると特徴的。

過去に aasm を使ったときは、だいたいほとんどの event に対して before で何かしらのタイムスタンプをいれるようにしていたんだけど(本当に必要だったかは要検討)、[transitions にはタイムスタンプをいれるためのサポート](https://github.com/troessner/transitions#timestamps)ある。

## さいごに

また、これを書いている間に新たな statemachine が誕生しそうな息吹を感じた。

今回、3 つの gem を比較してみたけど、今後とりあえず導入するなら transitions を選択してみるかなあ。
とはいえ、だいたい適当に選ぶとつらいめにあうので、ある程度その state をどう使いたいかという用途の検討はしてから選びたい。

あと、他におもしろいのあったら教えてください !