[RUBY] If there is a state transition, let's create a State class

If there is data that transitions to a state, this kind of code will occur frequently.

erb:view.html.erb


<%#It seems that approval cannot be made if the state of the report is 1 or 2, but what are the states of 1 and 2?%>
<%= button_tag('Approval', disabled: (report.state == 1 || report.state == 2)) %>

reports_controller.rb


def accept
  report = Report.find(params[:id])
  #The same conditional statement as view is required for handling!
  if report.state == 1 || report.state == 2
    flash[:error] = 'Cannot approve'
    redirect_to root_url
  end
  report.state = 3 #Approval value!
  report.save!
end

If you use a constant, it will improve a little, but ...

erb:view.html.erb


<%= button_tag('Approval', disabled: (report.state == Report::STATE_DRAFT || report.state == Report::STATE_TRASH)) %>

reports_controller.rb


def accept
  report = Report.find(params[:id])
  #After all, the same conditional statement as view is required!
  if report.state == Report::STATE_DRAFT || report.state == Report::STATE_TRASH
    flash[:error] = 'Cannot approve'
    redirect_to root_url
  end
  report.state = Report::STATE_APPROBAL
  report.save!
end

report.rb


class Report < ApplicationRecord
  #If there are many columns that require constants, many constants will be required.
  #Besides, the scope is too wide to see the purpose.
  STATE_DRAFT = 1
  STATE_TRASH = 2
  STATE_APPROBAL = 3
end

There is a way to use ActiveRecord :: Enum instead of a constant, but it doesn't solve the need for the same conditional statement in various places ... You can solve it by adding a method to the ActiveRecord model.

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

But this is the way to the Fat Model ... So ...

Let's create a class that manages state transitions

Avoid Fat Modeling of ActiveRecord models and create state transition classes with clear responsibilities. It would be nice if the conditional statement had a state change method called report_state.to_approval, with the feeling ofreport_state.approval_in_next?[^ 1](can it be in the approved state).

[^ 1]: The method name is intended to be "Is there approval in the next state?", But since it is an English shit coarse fish, there may be other good method names.

reports_controller.rb


def accept
  report = Report.find(params[:id])
  report_state = report.state_object
  #Clean
  unless report_state.approval_in_next?
    flash[:error] = 'Cannot approve'
    redirect_to root_url
  end
  #This is a simple example with only one column, but even if the state transition process is complicated, to_*Can be hidden in the method!
  new_report_state = report_state.to_approval
  report.state = new_report_state.state
  report.save!
end

report.rb


class Report < ApplicationRecord
  # state_object=It may be convenient to make a method
  def state_object
    ReportState.new(state)
  end
end

The actual ReportState class looks like this.

report_state.rb


class ReportState
  attr_accessor :state
  #Move constants from Report class. It fits better all the time
  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 'Illegal state transition!' unless approval_in_next?

    self.class.new(APPROBAL)
  end
end

Now, even if the conditions that can be approved become complicated or the state transition process to the approval status becomes complicated, all you have to do is change here!


This entry is the State Transition Diagram, State Transition Table of Requirements Analysis Driven Design. #% E7% 8A% B6% E6% 85% 8B% E9% 81% B7% E7% A7% BB% E5% 9B% B3% E7% 8A% B6% E6% 85% 8B% E9% 81% B7% E7% A7% BB% E8% A1% A8) has been reorganized and rewritten into general-purpose content. It's a long story, but it's a story about DDD-like things in Rails.

Recommended Posts

If there is a state transition, let's create a State class
If you have complicated calculations, let's create a CalcRule class
What is a wrapper class?
[Rails DM] Let's create a notification function when DM is sent!
Finally, create a method for whether there is any character
[Swift] What is "inheriting a class"?
Resultset's next () is not a "method to determine if there is a ResultSet next".
[Java] Conditional branching is an if statement, but there is also a conditional operator.
Resultset's next () was misunderstood as a method to determine if there is a next
What is a class in Java language (3 /?)
Is there a numeric version of include?
What is a class in Java language (1 /?)
Let's create a Java development environment (updating)
What is a class in Java language (2 /?)
Create a temporary class with new Object () {}
[Rails] Let's create a super simple Rails API
How to check if an instance variable is defined in a Ruby class
[Android] Inherit ImageView to create a new class
Let's create a REST API using WildFly Swarm.
Let's create a timed process with Java Timer! !!
Let's create a RESTful email sending service + client
Let's create a custom tab view in SwiftUI 2.0
How to create a class that inherits class information
Let's create a super-simple web framework in Java
[Java] Let's create a mod for Minecraft 1.14.4 [Introduction]
[Java] Let's create a mod for Minecraft 1.16.1 [Introduction]
[Java] Let's create a mod for Minecraft 1.14.4 [99. Mod output]