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[])
  end

  def create
    @survey = Survey.find(params[])
    if valid_recipients? && valid_message?
      recipient_list.each do |email|
        invitation = Invitation.create(
          @survey,
          current_user,
          email,
          'pending'
        )
        Mailer.invitation_notification(invitation, message)
      end
      redirect_to survey_path(@survey), '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[][]
  end

  def message
    params[][]
  end
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 , , , 

  validates , true
  validates , { 1 }
  validates , true
  validates , true

  validates_with EnumerableValidator,
    [],
    unless: 'recipients.nil?',
    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,
        sender,
        recipient_email,
        'pending'
      )
    end
  end

  def deliver_invitations
    create_invitations.each do |invitation|
      Mailer.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_inviter = SurveyInviter.new
  end

  def create
    @survey = Survey.find(params[])
    @survey_inviter = SurveyInviter.new(survey_inviter_params)

    if @survey_inviter.invite
      redirect_to survey_path(@survey), 'Invitation successfully sent'
    else
      render 'new'
    end
  end

  private

  def survey_inviter_params
    params.require().permit(
      ,
      
    ).merge(
      current_user,
      @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.

Work with us to make a new Rails app, or to maintain, improve, or scale your existing app.