Service Objects in Rails
I want to introduce you to an idea. You may have already heard about things like skinny controllers, skinny models, etc. But then you start coding and have to do something extra. It’s more than just User.new
and redirect_to user
. How are we supposed to keep those files small when we have to do all this stuff?! Answer: Service objects.
These are plain ruby classes. Ruby, not rails. Back in the day when you were writing things like a Greeting
class that spat out "hello world"
, that’s what we’re going to build. I emphasize this because it took far too long for that to work its way into my brain. It is a plain. Ruby. Class.
So let’s make one. Here’s the scenario: In your app, a user gets a random color assigned to them when they sign up.
Where do service objects go?
I usually create a directory called app/services
. This (for the most part) is where all of my plain ruby classes go. I know some people who also put them in app/models
. There’s no technical advantage to either, and they can actually go anywhere. It’s just an organizational preference. I prefer my models to be separate, some people think all Class-like files should be grouped together. Your choice. For this tutorial, I’ll be putting them in app/services
.
What should we call it?
My current preference is to name service objects as NounVerb. Example: If you have a class that that emails customers, you could name it CustomerMailer
. A class that calculates tax on a particular item could be named TaxCalculator
. Try to use sensible names, but you have to be pragmatic about it as well. UserSignerUpper
is terrible as a class name, so we’re going to use UserRegistrar
instead.
Code
We’ll set up the model and controller first:
app/models/user.rb
class User < ActiveRecord::Base
validates :email, presence: true, uniqueness: true
end
app/controllers/userscontrollers.rb_
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = UserRegistrar.perform(user_params)
if @user.save
redirect_to @user
else
render :new
end
end
def show
@user = User.find(params[:id])
end
private
def users_params
params.require(:user).permit(:email)
end
end
Pretty basic. Notice the @user = UserRegistrar.perform(user_params)
line. This is the service object (that we haven’t written yet) in action. I like writing the code for how I will use the class before the class itself. It gives me a goal for when I’m writing it. In this case, it tells me:
- It needs to take
user_params
- It needs to have a
.perform
method - It needs to return a fully formed user
Let’s write it.
app/services/userregistrar.rb_
class UserRegistrar
COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'violet'].freeze
attr_reader :params
def initialize(params)
@params = params
end
def self.perform(params)
new(params).perform
end
def perform
User.new(user_params)
end
private
def user_params
{
color: random_color,
email: params[:email]
}
end
def random_color
COLORS.sample
end
end
Ok so here’s our service object. Again, plain old ruby object, often called by the funny sounding acronym PORO. No rails in sight. Walking through the object:
COLORS = ['red', 'orange', ...].freeze
A constant named COLORS
holds all of our color values. I decided to call .freeze
on it at the end because those values never, ever change. For this use case, we will always have those six colors. In general if you’re going to use a constant, freeze it. If you need something that can change, use a variable or attribute, not a constant.
attr_reader :params
This is so we can have easy access to whatever I set @params
to later. You’ll see in a sec.
def initialize(params)
@params = params
end
We set @params
to the user parameters we pass in from the controller. I always structure initialization around only setting those attr_*
variables. That’s all it does. Nothing more.
def self.perform(params)
new(params).perform
end
def perform
User.new(user_params)
end
This is a little funky. The point of this is so I can call UserRegistrar.perform(user_params)
instead of UserRegistrar.new(user_params).perform
.
self.perform
calls new
and passes in user_params
. Now we have an instance of UserRegistrar
as opposed to the class. Then it immediately calls .perform
on that instance, because .perform
is an instance method, not a class method, such as self.perform
. Convenience methods like these let your code look nicer.
.perform
creates a new user object with some data (we’ll get to that next) and returns it. No magic, just User.new
.
So that’s all the boring part. A bit of setup code, but we wanted to randomly assign a color! One more step first:
private
def user_params
{
color: random_color,
email: params[:email]
}
end
This is a convenience method letting us pass user_params
to User.new
back up in the .perform
method. Astute readers may wonder “Why not just use params.merge(color: random_color)
?”. Imagine a Company with a full address and a name and an industry and a stock symbol and… The number of parameters could be very large. We can mix values and other methods here to make up everything we need for User
. .merge
would be fine in this case, but messy in others, so I’m keeping it scaleable.
random_color
gets our random color (duh) for us, and params[:email]
is the email we got when we initialized the instance. All said and done, this is all the data a user needs to be whole.
def random_color
COLORS.sample
end
And finally, our random color.
A note on private
:
I use private when there’s no reason for anything outside the class to access the method. Nothing else outside of this class will call random_color
so it goes in private
. For the purpose of this example, I kept COLORS
and attr_reader :params
on the public side, but I often make those private too.
You’ll notice too that many of these methods only do one thing, like random_color
. I like these methods because while I could put that code in user_params
directly, random_color
gives me more information. Using small, well named methods is a HUGE win for code readability. No one has to guess what random_color
does.
So that’s service objects. It can be a little heady at first, but you’ll start seeing more and more reasons to use them in project. Check them out and your code will get cleaner.
You can see example code here: https://github.com/edwardloveall/examples/tree/service-objects