Rails Service Objects - Why and How

There seems to be some confusion about the role of the ‘controller’ and ‘model’ among many Rails developers. Today I was inspecting the code of a project I am planning to take over by January 2018. One controller almost had 500 lines of code and a model had 1300 lines of code, a ton of fat methods with really ugly naming.

I believe controllers and models should be skinny. Any business logic should go to the service objects and any validation logic should go to form objects. To show how serious I am on the topic, lets work on a simple website with a contact form.

class ContactsController < ApplicationController
  def show
    # Ok, I am thinking of a form object here. It can be a modal.
    # Doesn't really matter.
    @contact_form = ContactForm.new
  end

  def create
    # What to do here?
  end

  private

  def contact_params
    # For starters, that * down there is the splat operator.
    params.require(:contact_form).permit(*ContactForm::PERMITTED_ATTRIBUTES)
  end
end

While the job at hand is really simple, we can have a create action like this

def create
  @contact_form = ContactForm.new(contact_params)
  if @contact_form.valid?
    ContactMailer.notify_admin(
      @contact_form.email,
      @contact_form.first_name,
      @contact_form.last_name,
      @contact_form.message
    ).deliver_later

    ContactMailer.notify_user(
      @contact_form.email,
      @contact_form.first_name
    ).deliver_later

    flash[:success] = 'Message sent!'
    redirect_to root_path
  else
    flash[:warning] = 'Message not sent, check form for errors!'
    render :show
  end
end

Its simple enough, but why is business logic in the controller? Lets clean it up!

def create
  @contact_form = ContactForm.new(contact_params)
  if @contact_form.valid?
    ContactService.process(@contact_form)
    flash[:success] = 'Message sent!'
    redirect_to root_path
  else
    flash[:warning] = 'Message not sent, check form for errors!'
    render :show
  end
end

Really? You want me to spoon feed you too? Ok! Alright!

class ContactService
  attr_reader :contact_form

  def initialize(contact_form)
    @contact_form = contact_form
  end

  def process
    # blah!
  end

  def self.process(contact_form)
    new(contact_form).process
  end
end

I have been said “Who uses process? Use run!”. Now let me point out why I use #process instead of #run.

ContactService.process(@contact_form)

The above sounds “Contact service process contact form”. If it was run, “Contact service run contact form”. I don’t know about you but it doesn’t sound right to me.

Thats it, happy hacking!