Ruby Science
Introduce Form Object
This is a specialized type of extract class that is used to remove business logic from controllers when processing data outside of an ActiveRecord model.
Uses
- Keeps business logic out of controllers and views.
- Adds validation support to plain old Ruby objects.
- Displays form validation errors using Rails conventions.
- Sets the stage for extract validator.
Example
The create
action of our
InvitationsController
relies on user-submitted data for
message
and recipients
(a comma-delimited list
of email addresses).
It performs a number of tasks:
- Finds the current survey.
- Validates that the
message
is present. - Validates each of the
recipients
’ email addresses. - Creates an invitation for each of the recipients.
- Sends an email to each of the recipients.
- Sets view data for validation failures.
# app/controllers/invitations_controller.rb
class InvitationsController < ApplicationController
EMAIL_REGEX = /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/
def new
@survey = Survey.find(params[:survey_id])
end
def create
@survey = Survey.find(params[:survey_id])
if valid_recipients? && valid_message?
.each do |email|
recipient_list= Invitation.create(
invitation survey: @survey,
sender: current_user,
recipient_email: email,
status: 'pending'
)Mailer.invitation_notification(invitation, message)
end
@survey), notice: 'Invitation successfully sent'
redirect_to survey_path(else
@recipients = recipients
@message = message
'new'
render end
end
private
def valid_recipients?
.empty?
invalid_recipientsend
def valid_message?
.present?
messageend
def invalid_recipients
@invalid_recipients ||= recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
itemend
end.compact
end
def recipient_list
@recipient_list ||= recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end
def recipients
[:invitation][:recipients]
paramsend
def message
[:invitation][:message]
paramsend
end
By introducing a form object, we can move the concerns of data
validation, invitation creation and notifications to the new model
SurveyInviter
.
Including ActiveModel::Model allows us to leverage the familiar active record validation syntax.
As we introduce the form object, we’ll also extract an enumerable
class RecipientList
and validators
EnumerableValidator
and EmailValidator
. These
will be covered in the Extract Class and Extract Validator
chapters.
# app/models/survey_inviter.rb
class SurveyInviter
include ActiveModel::Model
attr_accessor :recipients, :message, :sender, :survey
:message, presence: true
validates :recipients, length: { minimum: 1 }
validates :sender, presence: true
validates :survey, presence: true
validates
EnumerableValidator,
validates_with attributes: [:recipients],
unless: 'recipients.nil?',
validator: EmailValidator
def recipients=(recipients)
@recipients = RecipientList.new(recipients)
end
def invite
if valid?
deliver_invitationsend
end
private
def create_invitations
.map do |recipient_email|
recipientsInvitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending'
)end
end
def deliver_invitations
.each do |invitation|
create_invitationsMailer.invitation_notification(invitation, message).deliver
end
end
end
Moving business logic into the new form object dramatically reduces
the size and complexity of the InvitationsController
. The
controller is now focused on the interaction between the user and the
models.
# app/controllers/invitations_controller.rb
class InvitationsController < ApplicationController
def new
@survey = Survey.find(params[:survey_id])
@survey_inviter = SurveyInviter.new
end
def create
@survey = Survey.find(params[:survey_id])
@survey_inviter = SurveyInviter.new(survey_inviter_params)
if @survey_inviter.invite
@survey), notice: 'Invitation successfully sent'
redirect_to survey_path(else
'new'
render end
end
private
def survey_inviter_params
.require(:survey_inviter).permit(
params:message,
:recipients
.merge(
)sender: current_user,
survey: @survey
)end
end
Next Steps
- Check that the controller no longer has long methods.
- Verify the new form object is not a large class.
- Check for places to re-use any new validators if extract validator was used during the refactoring.
Ruby Science
The canonical reference for writing fantastic Rails applications from authors who have created hundreds.