RailsはActiveRecordでデータを永続化します。そのデータが状態を保持するならオートマトンが有効です。
オートマトンのライブラリはいくつかありますが、シンプルなStatefulEnumを紹介します。
environment
- Ruby: 2.3.3
- Rails: 5.0.1
- Node.js: 6.9.2
finite automaton
有限オートマトンは有限状態機械(finite state machine)とも呼ばれ、有限個の 状態(state) と 遷移の規則(rule) と 入力(input) を組み合わせたモデルです。状態遷移が理解しやすく、また推測しやすくなります。
- 状態1で文字aを読むと、状態2に移動する。
- 状態2で文字aを読むと、状態1に移動する。
rails new
まず、Railsでデータベースを作成します。
-
rails generate model
コマンドで user を作成します。
$ rails new rails_stateful_enum
$ cd rails_stateful_enum/
$ rails generate model user name status:integer --no-timestamps
-
create_table
のstatus
にdefault: 0
を設定します。
db/migrate/[datetime]_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name
t.integer :status, default: 0
end
end
end
-
rails db:migrate
コマンドでデータベースを作成します。
$ rails db:migrate
enum
次は、ActiveRecord::Enumのおさらいです。
- ActiveRecord で列挙型を利用することができます。
-
enum
に 属性の名前 と 属性の値 を設定します。
app/models/user.rb
class User < ApplicationRecord
enum status: {active: 0, inactive: 1, closed: 2}
end
- インスタンスメソッドに 属性の値の確認 や 属性の値の更新 が追加されます。
$ rails console
> user = User.create
# INSERT INTO "users" DEFAULT VALUES
=> #<User id: 1, name: nil, status: "active">
> user.status
=> "active"
# 属性の値の確認
> user.active?
=> true
# 属性の値の更新
> user.inactive!
# UPDATE "users" SET "status" = ? WHERE "users"."id" = ? [["status", 1], ["id", 1]]
=> true
> user.status
=> "inactive"
- クラスメソッドに 属性の値の一覧 や 属性の値のスコープ が追加されます。
$ rails console
# 属性の値の一覧
> User.statuses
=> {"active"=>0, "inactive"=>1, "closed"=>2}
# 属性の値のスコープ
> User.active
# SELECT "users".* FROM "users" WHERE "users"."state" = ? [["state", 0]]
stateful enum
そして、StatefulEnum でオートマトンを実装します。
-
bundle
コマンドでstateful_enum
を追加します。-
graphviz
は図形の出力に利用します。
-
Gemfile
gem 'stateful_enum'
gem 'ruby-graphviz'
$ bundle
-
enum
にブロックで イベント(event) と 遷移(transition) を設定します。- 属性の値 がオートマトンの 状態(state)
- イベント(event) がオートマトンの 入力(input)
- 遷移(transition) オートマトンの 規則(rule)
app/models/user.rb
class User < ApplicationRecord
enum status: {active: 0, inactive: 1, closed: 2} do
event :disable do
transition :active => :inactive
end
event :enable do
transition :inactive => :active
end
event :close do
transition [:active, :inactive] => :closed
end
end
end
- インスタンスメソッドに イベントの 属性の値の更新 が追加されます。
- インスタンスメソッドに イベントの可能を確認 や イベントの遷移後の値 が追加されます。
$ rails console
> user = User.create
# 属性の値の更新
> user.disable
# UPDATE "users" SET "status" = ? WHERE "users"."id" = ? [["status", 1], ["id", 2]]
=> true
> user.status
=> "inactive"
# イベントの可能を確認
> user.can_enable?
=> true
# イベントの遷移後の値
> user.enable_transition
=> :active
diagram
- 状態遷移図を出力したり、PlantUMLのテキストを出力することができます。
$ mkdir -p doc/diagrams
$ DEST_DIR=doc/diagrams bin/rails generate stateful_enum:graph user
$ DEST_DIR=doc/diagrams bin/rails generate stateful_enum:plantuml user
-
stateful_enum:graph
はdoc/diagrams/User.png
に出力されます。
-
stateful_enum:plantuml
はdoc/diagrams/User.puml
に出力されます。
doc/diagrams/User.puml
[*] --> active
active --> inactive :disable
inactive --> active :enable
active --> closed :close
inactive --> closed :close
closed --> [*]
gulp
状態遷移図は gulp で自動化にすると捗ります。
$ yarn init --yes
-
scripts
のstart
にgulp
を設定します。
package.json
{
"name": "rails_stateful_enum",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "gulp"
}
}
$ yarn add --dev gulp gulp-watch
$ touch gulpfile.js
- gulp-watch は新規ファイルも監視対象なので便利です。
gulpfile.js
const gulp = require('gulp')
const gutil = require('gulp-util')
const watch = require('gulp-watch')
const spawn = require('child_process').spawn
gulp.task('watch', () => {
watch('app/models/**/*.rb', (event) => {
gutil.log(event.path.replace(event.cwd, 'watching: '))
spawn('DEST_DIR=doc/diagrams bin/rails generate stateful_enum:plantuml', [event.stem], {shell:true})
})
watch('doc/**/*.puml', (event) => {
console.log(event.contents.toString())
})
})
gulp.task('default', ['watch'])
-
yarn start
コマンドで状態遷移図が自動で出力されるようになりました。
$ yarn start
- Atom は PlantUML Viewer で PlantUML のテキストを状態遷移図に出力することができます。
- gulp と組み合わせて、コードを修正すると状態遷移図が変更されるようになりました。
draw uml
- Rails でも PlantUML Viewer で PlantUML を表示することができます。
Gemfile
gem 'draw_uml'
-
routes
にEngine
をマウントします。
config/routes.rb
Rails.application.routes.draw do
mount DrawUml::Engine, at: '/rails/draw/uml' if Rails.env.development?
end
$ bundle
$ rails server
- http://localhost:3000/rails/draw/uml/User にアクセスすると状態遷移図が表示されます。
browser sync
- gulp で browser-sync を利用すると、状態遷移図が自動で表示することができます。
$ yarn add --dev browser-sync
-
localhost:3000
をproxy
に設定します。
gulpfile.js
const gulp = require('gulp')
const gutil = require('gulp-util')
const watch = require('gulp-watch')
const spawn = require('child_process').spawn
const browserSync = require('browser-sync')
gulp.task('browser-sync', () => {
return browserSync.init(null, {
proxy: 'localhost:3000',
reloadDelay: 1000
})
})
gulp.task('watch', () => {
watch('app/models/**/*.rb', (event) => {
gutil.log(event.path.replace(event.cwd, 'watching: '))
spawn('DEST_DIR=doc/diagrams bin/rails generate stateful_enum:plantuml', [event.stem], {shell:true})
browserSync.reload()
})
watch('doc/**/*.puml', (event) => {
console.log(event.contents.toString())
})
})
gulp.task('default', ['browser-sync', 'watch'])
-
yarn start
コマンドで状態遷移図が自動で出力されるようになりました。
$ yarn start
自動化は楽しくて楽ですね。
Tips
draw smd
DrawUML 以外に DrawERD と DrawSMD を利用すると Rails で ER図 や 状態遷移図 を出力することができます。
Gemfile
gem 'draw_erd'
gem 'draw_smd'
gem 'state_machine'
-
routes
にEngine
をマウントします。
config/routes.rb
Rails.application.routes.draw do
mount DrawUml::Engine, at: '/rails/draw/uml' if Rails.env.development?
mount DrawErd::Engine, at: '/rails/draw/erd' if Rails.env.development?
mount DrawSmd::Engine, at: '/rails/draw/smd' if Rails.env.development?
end
$ bundle
$ rails server
- http://localhost:3001/rails/draw/erd にアクセスするとER図が表示されます。
- http://localhost:3001/rails/draw/smd にアクセスすると状態遷移図が表示されます。
state machine
-
rails generate model
コマンドで group を作成します。
$ rails generate model group name state
$ rails generate model member user:references group:references
$ rails db:migrate
- state_machine のブロックは StatefulEnum と同じです。
app/models/group.rb
class Group < ApplicationRecord
has_many :users, through: :members
has_many :members
state_machine :state, initial: :active do
event :disable do
transition :active => :inactive
end
event :enable do
transition :inactive => :active
end
event :close do
transition [:active, :inactive] => :closed
end
end
end
enjoy automaton