Edward Loveall

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:

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