LoginSignup
1
0

More than 3 years have passed since last update.

状態遷移があるならStateクラスを作ろう

Posted at

状態遷移するデータがあるとこんなコードが頻発しますよね。

view.html.erb
<%# reportのstateが1か2だったら承認ができないみたいだけど、1と2ってどういう状態なの? %>
<%= button_tag('承認', disabled: (report.state == 1 || report.state == 2)) %>
reports_controller.rb
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

定数を使えば少し改善するけど・・・

view.html.erb
<%= button_tag('承認', disabled: (report.state == Report::STATE_DRAFT || report.state == Report::STATE_TRASH)) %>
reports_controller.rb
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
report.rb
class Report < ApplicationRecord
  # 定数が必要なカラムが多いと定数がたくさん必要になる
  # それにスコープが広すぎて用途が見えづらい
  STATE_DRAFT = 1
  STATE_TRASH = 2
  STATE_APPROBAL = 3
end

定数ではなく、ActiveRecord::Enumを使う方法もあるけど、同じ条件文がいろいろな場所で必要なことは解決しない・・・。ActiveRecordモデルにメソッドを追加すれば一応解決できる。

report.rb
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という状態変更のメソッドもあると嬉しい。

reports_controller.rb
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
report.rb
class Report < ApplicationRecord
  # state_object=メソッドも作ると便利かも
  def state_object
    ReportState.new(state)
  end
end

実際のReportStateクラスはこんな感じ。

report_state.rb
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っぽいことする話です。


  1. 「次の状態の中に承認はあるか」というつもりのメソッド名なのだけど、英語クソ雑魚なので他によいメソッド名があるかも 

1
0
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
1
0