ナガモト の blog

Full Cycle Developerを目指すエンジニアが有用そうな技術記事や、ポエムのようなよしなしごとを投稿するブログです。

Ruby on Railsで状態を扱うStatefulEnum gem

StatefulEnumというgemを紹介されたので使ってみました。他のgemと比較して違いや使い心地も伝えたいと思います。

以前書いた類似gemの紹介記事はこちら ngmt83.hatenablog.com

SNSのアカウントを想定したAccountモデルを例に実装します。

リポジトリはこちらです。 github.com

クラス図

f:id:ngmt83:20190212100220p:plain
Account関連クラス図

ソース

状態遷移図

f:id:ngmt83:20190212100252p:plain
Account状態遷移図

ソース

クラス図や状態遷移図はPlantUMLで作成しています。 ngmt83.hatenablog.com

ActiveRecord::Enumで状態を設定する

Accountモデルにintegerstatusカラムを追加

ActiveRecord::Enumでは状態を表すinteger型のカラムが必要です。カラムがなければマイグレーションで作成しましょう。railsコマンドであれば次のコマンドでカラムが追加できます。

rails g migration AddColumnToAccount status:integer

実際に作成したマイグレーションファイルはこちらです。

class AddColumnToAccount < ActiveRecord::Migration[5.2]
  def change
    add_column :accounts, :status, :integer, null: false, default: 0
    add_index :accounts, :status
  end
end

statusを持たないものは存在しない前提でnull: falseと初期状態として0を指定し、statusによる絞り込みを想定してインデックスを追加しています。*1

状態を定義

次のような記述方法で状態を定義します。

enum status: { registered: 0, active: 1, suspended: 2, banned: 3, inactive: 4 }

ActiveRecord::Enumはステータスを整数に変換してDBに保存するため、どのステータスが整数で何にあたるのかを定義しています。 *2

この定義により、次のようなメソッドが使用可能になります。

# account は Accountのインスタンス
account.suspended! # statusをsuspendedに変更する
account.suspended? # statusがsuspendedかどうかを真偽値で返す
account.status # statusを文字列で返す

課題

ActiveRecord::Enumのみでは次のような課題が存在します。

  • 状態遷移を表すメソッド(hoge!)の名前が動詞節にならない(hogeは状態を表す単語であり過去分詞形か形容詞)
  • 状態遷移の前と後の状態をわかりやすく制限できない
  • 状態遷移の条件を設定できない

これらを解決できるのがStatefulEnum gemです。

StatefulEnum gemを導入する

gemインストール

Gemfileに下記の通り記述し、

gem 'stateful_enum'

bundlerでインストールしましょう。

bundle install

状態遷移の定義

状態遷移を伴う操作をeventとし、transitionsで状態遷移を記述します。

  has_one :active_suspension, lambda { where(removed_at: nil) }, class_name: 'AccountSuspension'
# 中略
    event :suspend do
      before do
        suspensions.create!
      end
      transition :active => :suspended, unless: -> { active_suspension }
    end

https://github.com/nagamoto/state-machine-sample/blob/a8d701446476f27ad7ac2ac54ecf8c4a8fcf937c/app/models/account.rb#L17

このコードを解説すると次のとおりです。

active状態で、active_suspensionがfalseとなる場合のみsuspendというイベントを実行可能。実行時すると関連モデルsuspensionを生成し、suspended状態に遷移する。」

定義されるメソッド

# account は Accountのインスタンス
account.suspend # suspendイベントを実行する。事前条件を満たさない場合falseを返す
account.suspend! # suspendイベントを実行する。事前条件を満たさない場合<RuntimeError: Invalid transition>をraiseする
account.can_suspend? # suspendを実行できるかどうか(事前条件の結果)を真偽値で返す
account.suspend_transition # suspend時に遷移する状態を返す

事前条件は次のとおり

account.active? && !account.active_suspension

使えなくなるメソッド

account.suspended! # NoMethodError

標準のEnumが定義する状態遷移メソッドは使用できなくなっています。動詞節のメソッドで状態遷移したいがためにStatefulEnumを利用しているので、むしろありがたいです。

比較

次の2つのgemを比較しました。

StatefulEnum gemのメリット

AASM gemのメリット

  • モデル内で状態の初期値を設定できる
  • include AASMしたモデルでのみ有効化できる
  • 状態をstringで保持することもできる
  • コールバック・ロック・トランザクションなど豊富な詳細設定が可能

所感(使い分け方)

どちらも使い心地はよかったです。

とはいえ、基本方針としてRails wayに乗って継続的に開発するために、可能な限りシンプルなgemのみを使用した方がいいと考えているため、豊富な詳細設定が不要であればStatefulEnumがいいかと思います。また、コールバック・ロック・トランザクションを細かく取り扱おうとするとモデルにすべておさまるとは思えません。*5そのため、細かく取り扱う場合はドメインロジックを別クラスに移譲することになると思うので、詳細設定が必要でもシンプルなStatefulEnumでいいのではないかと思います。

例外として、状態をstringで保持したい場合にはStatefulEnumでは対応できないため、AASMを用いていいかと思います。*6

*1:https://github.com/nagamoto/state-machine-sample/blob/a8d701446476f27ad7ac2ac54ecf8c4a8fcf937c/db/migrate/20190211150628_add_column_to_account.rb

*2:https://github.com/nagamoto/state-machine-sample/blob/05c16fe03276b80e76dd857e12bea12927de9923/app/models/account.rb#L9

*3:ngmt83.hatenablog.com

*4:日本人エンジニア的には何かあったときに聞きやすい

*5:ファットモデルは避けたい

*6:DB上でも状態がわかりやすいのは大きなメリット。その代わりパフォーマンスが多少落ちる可能性はある