If there is data that transitions to a state, this kind of code will occur frequently.
<%#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)) %>
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 ...
<%= button_tag('Approval', disabled: (report.state == Report::STATE_DRAFT || report.state == Report::STATE_TRASH)) %>
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
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.
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 ...
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 of
report_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.
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
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.
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.