LoginSignup
24
17

More than 5 years have passed since last update.

Rails の 有限オートマトン

Posted at

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_tablestatusdefault: 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:graphdoc/diagrams/User.png に出力されます。

graphviz.png

  • stateful_enum:plantumldoc/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
  • scriptsstartgulp を設定します。
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 と組み合わせて、コードを修正すると状態遷移図が変更されるようになりました。

https://gyazo.com/b4114b9b70bd24e9e458ed5373bb22b7

draw uml

  • Rails でも PlantUML Viewer で PlantUML を表示することができます。
Gemfile
gem 'draw_uml'
  • routesEngine をマウントします。
config/routes.rb
Rails.application.routes.draw do
  mount DrawUml::Engine, at: '/rails/draw/uml' if Rails.env.development?
end
$ bundle
$ rails server

browser sync

  • gulp で browser-sync を利用すると、状態遷移図が自動で表示することができます。
$ yarn add --dev browser-sync
  • localhost:3000proxy に設定します。
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

自動化は楽しくて楽ですね。:blush:

Tips

draw smd

DrawUML 以外に DrawERDDrawSMD を利用すると Rails で ER図 や 状態遷移図 を出力することができます。

Gemfile
gem 'draw_erd'
gem 'draw_smd'
gem 'state_machine'
  • routesEngine をマウントします。
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

state machine

  • rails generate model コマンドで group を作成します。
$ rails generate model group name state
$ rails generate model member user:references group:references
$ rails db:migrate
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

24
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
17