状態遷移するデータがあるとこんなコードが頻発しますよね。
<%# reportのstateが1か2だったら承認ができないみたいだけど、1と2ってどういう状態なの? %>
<%= button_tag('承認', disabled: (report.state == 1 || report.state == 2)) %>
def accept
report = Report.find(params[:id])
# ハンドリングのためにviewと同じ条件文が必要になる!
if report.state == 1 || report.state == 2
flash[:error] = '承認できません'
redirect_to root_url
end
report.state = 3 # 承認の値!
report.save!
end
定数を使えば少し改善するけど・・・
<%= button_tag('承認', disabled: (report.state == Report::STATE_DRAFT || report.state == Report::STATE_TRASH)) %>
def accept
report = Report.find(params[:id])
# 結局viewと同じ条件文が必要になる!
if report.state == Report::STATE_DRAFT || report.state == Report::STATE_TRASH
flash[:error] = '承認できません'
redirect_to root_url
end
report.state = Report::STATE_APPROBAL
report.save!
end
class Report < ApplicationRecord
# 定数が必要なカラムが多いと定数がたくさん必要になる
# それにスコープが広すぎて用途が見えづらい
STATE_DRAFT = 1
STATE_TRASH = 2
STATE_APPROBAL = 3
end
定数ではなく、ActiveRecord::Enumを使う方法もあるけど、同じ条件文がいろいろな場所で必要なことは解決しない・・・。ActiveRecordモデルにメソッドを追加すれば一応解決できる。
class Report < ApplicationRecord
STATE_DRAFT = 1
STATE_TRASH = 2
STATE_APPROBAL = 3
def can_accept?
state != STATE_DRAFT && state != STATE_TRASH
end
end
でも、これはFat Modelへ続く道だ・・・。
なので・・・
状態遷移を管理するクラスを作ろう
ActiveRecordモデルのFat Model化を回避して、責務が明確な状態遷移のクラスを作りましょう。
条件文はreport_state.approval_in_next?
1(承認状態にできるか)という感じにして、report_state.to_approval
という状態変更のメソッドもあると嬉しい。
def accept
report = Report.find(params[:id])
report_state = report.state_object
# すっきり
unless report_state.approval_in_next?
flash[:error] = '承認できません'
redirect_to root_url
end
# これはカラムが1つだけの単純な例だけど、状態遷移の処理が複雑でもto_*メソッドに隠蔽できる!
new_report_state = report_state.to_approval
report.state = new_report_state.state
report.save!
end
class Report < ApplicationRecord
# state_object=メソッドも作ると便利かも
def state_object
ReportState.new(state)
end
end
実際のReportStateクラスはこんな感じ。
class ReportState
attr_accessor :state
# Reportクラスから定数を引越し。ずっと納まりが良くなってる
DRAFT = 1
TRASH = 2
APPROBAL = 3
def initialize(state)
self.state = state
end
def approval_in_next?
state != DRAFT && state != TRASH
end
def to_approval
raise '不正な状態遷移!' unless approval_in_next?
self.class.new(APPROBAL)
end
end
これで、承認できる条件が複雑になったり、承認ステータスへの状態遷移処理が複雑化してもここを変更するだけでよくなりました!
このエントリは要求分析駆動設計の状態遷移図、状態遷移表を再編し汎用的な内容に書き換えたものです。
めっちゃ長いけど、RailsでDDDっぽいことする話です。
-
「次の状態の中に承認はあるか」というつもりのメソッド名なのだけど、英語クソ雑魚なので他によいメソッド名があるかも ↩