Rails Service Objects - Why and How15 Nov 2017
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
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!