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
messageis 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?
recipient_list.each do |email|
invitation = Invitation.create(
survey: @survey,
sender: current_user,
recipient_email: email,
status: 'pending'
)
Mailer.invitation_notification(invitation, message)
end
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
else
@recipients = recipients
@message = message
render 'new'
end
end
private
def valid_recipients?
invalid_recipients.empty?
end
def valid_message?
message.present?
end
def invalid_recipients
@invalid_recipients ||= recipient_list.map do |item|
unless item.match(EMAIL_REGEX)
item
end
end.compact
end
def recipient_list
@recipient_list ||= recipients.gsub(/\s+/, '').split(/[\n,;]+/)
end
def recipients
params[:invitation][:recipients]
end
def message
params[:invitation][:message]
end
endBy 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
validates :message, presence: true
validates :recipients, length: { minimum: 1 }
validates :sender, presence: true
validates :survey, presence: true
validates_with EnumerableValidator,
attributes: [:recipients],
unless: 'recipients.nil?',
validator: EmailValidator
def recipients=(recipients)
@recipients = RecipientList.new(recipients)
end
def invite
if valid?
deliver_invitations
end
end
private
def create_invitations
recipients.map do |recipient_email|
Invitation.create!(
survey: survey,
sender: sender,
recipient_email: recipient_email,
status: 'pending'
)
end
end
def deliver_invitations
create_invitations.each do |invitation|
Mailer.invitation_notification(invitation, message).deliver
end
end
endMoving 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
redirect_to survey_path(@survey), notice: 'Invitation successfully sent'
else
render 'new'
end
end
private
def survey_inviter_params
params.require(:survey_inviter).permit(
:message,
:recipients
).merge(
sender: current_user,
survey: @survey
)
end
endNext 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.