Disclaimer: This is an ongoing project, so any feedback is greatly appreciated. I will update this article where appropriate. If you’d like to contribute to the ObjectServices project, please, feel free to shoot me a pull request.

We’re going through somewhat of a code renaissance at StatusPage right now. Scott’s been heads down, shipping product for the last two years and as we’re starting to scale the team, we’ve identified a few areas that we’d like to improve. I’m not the first one that’s gotten sick of the phrase “skinny controllers fat models.” It’s just plain impractical. Sure we can move the logic out of the controllers, but why? To dump it in the models? What purpose does that serve, besides cleaning up your controllers at the expense of polluting your models. It’s like picking up the pile of dirty laundry off your bed and hiding it in your closet.

It’s time to make some decisions on how we want to do our laundry, fold it and put it away. * so we can get it dirty and not put it away later

I think the statement that should trump all other half-baked rails organizational, motivational - whatever - statements, is “keep it testable.” Whatever you do, however you do it, whether you call it a concern, a service, or a monkey patch, if you can write clean, thorough and fast tests around it, you’re gonna get an A from me. One of the greatest lessons I’ve learned in my journey as an engineer is how good tested code makes me feel. So that’s my number one motivation and what we’re really focusing on during these refactor projects - how we can optimize and increase our coverage.

So, the first, biggest area for improvement is controllers. What tools are we implementing to help us organize, you ask?

Services

I’m sure you’ve heard of them. Many of you have probably implemented them. I think they’re a small, yet important part of a well organized code base. They’re transactional, in that their purpose is to transform input into functional state changes for your models over the lifecycle of a single request. So, they should initialize with a model, take some params and attempt to update that model. If they can’t update, log errors and return. They’re the glue that holds your controllers and models together, and they help decide how the controller should respond. This, to me, is all a service should do.

Let’s look at some code:

class UsersController < ApplicationController

  def update
    @user = current_user
    respond_to do |format|
      if user_params[:password].present?
        if @user.try(:authenticate, user_params[:current_password]) && user_params[:password] == user_params[:password_confirmation]
          user_params.delete(:current_password)
        else
          user_params.delete(:current_password)
          user_params.delete(:password)
          user_params.delete(:password_confirmation)
          format.html { redirect_to my_path, notice: 'There was a problem updating your password' }
          format.json { render json: { errors: ['There was a problem updating your password'] }, status: :unprocessable_entity }
        end
      end
      if @user.update(user_params)
        format.html { redirect_to my_path, notice: 'Profile updated.' }
        format.json { render json: { errors: ['Profile updated.'] }, status: :ok }
      else
        format.html { redirect_to my_path, notice: 'Could not update your profile.' }
        format.json { render json: { errors: ['Could not update your profile.'] }, status: :unprocessable_entity }
      end
    end
  end

end

And the associated tests:

describe UsersController do

  before do
    @user = create(:user, password: 'test')
  end

  describe '#update' do

    it 'should update the password' do
      patch :update, { current_password: 'test', password: 'something', password_confirmation: 'something' }, { user_id: @user.id )
      assert_redirected_to my_path
      assert @user.reload.authenticate('something')
    end

    it 'should update your email' do
      patch :update, { email: 'tyler@tmd.io' }, { user_id: @user.id }
      @user.reload
      assert_redirected_to my_path
      @user.email.must_be 'tyler@tmd.io'
    end

  end
end

There’s a couple things that annoy me about this. The big, glaring issue is the update method has a ton of responsibility. It’s ambiguous. It’s checking to see if the password param is present, then authenticating. It’s updating the model. It’s deciding how and where to redirect to, and what errors to return. It’s a ton of crap for a controller to be thinking about. Maybe, I’m… No, I’m definitely OCD. My OCD self looks at this and gets a little sad. My OCD self says, your controllers shouldn’t have to worry about what to update and how. They should really just be returning arbitrary data and status, and deciding what format that should be in.

Lets see how we can refactor this code, using a service object.

I’ve written a gem called object_services which gives you a minimal framework for incorporating services in to your workflow. It’s a rails plugin that includes generators for stubbing services and their tests. Emphasis on minimal.

All of our services extend the ObjectServices::Base class.

An example service extending the service base:

class UserService < ObjectServices::Base

  def update(params)
    if params[:password].present?
      update_password(params[:current_password], params[:password], params[:password_confirmation])
    else
      update_model(params)
    end
  end

  def update_password(current_password, password, confirmation)
    @errors << I18n.t('services.user.errors.password.update') if @model.nil? || current_password.nil? || password.nil? || confirmation.nil?
    @errors << I18n.t('services.user.errors.password.match') if password != confirmation
    @errors << I18n.t('services.user.errors.password.current') unless @model.try(:authenticate, current_password)
    update_model({ password: password, password_confirmation: confirmation })
  end

end

Finally, the UserService implemented in our controller:

class UsersController < ApplicationController

  def update
    respond_to do |format|
      @service = UserService.new(current_user)
      if @service.update(user_params)
        format.html { redirect_to my_path, notice: 'User successfully updated' }
        format.json { render json: @service.model.to_json, status: :ok }
      else
        format.html { redirect_to my_path, notice: @service.errors }
        format.json { render json: { errors: @service.errors }, status: :unprocessable_entity }
      end
    end
  end

end

Now that the controller can relax, we can test all the logic by unit testing the service:

describe UserService do

  before do
    @user = create(:user, password: 'testpw')
    @service = UserService.new(@user)
  end

  it 'should update a user model correctly' do
    @service.update(first_name: 'tyler')
    @user.reload
    @user.first_name.must_equal 'tyler'
  end

  it 'should update password' do
    @service.update({ current_password: 'testpw', password: 'something', password_confirmation: 'something' })
    @service.errors.must_be_empty
    assert @user.reload.authenticate('something')
  end

  it 'should not update password without your current password' do
    @service.update({ password: 'something', password_confirmation: 'something' })
    refute @user.reload.authenticate('something')
    assert @service.errors.any?
  end

end

A couple things to think about: I think you could argue that a service should have one specific responsibility. You would then have a single service for each action you’re implementing. A good example of this is Manuel Meurer’s services gem. I don’t know what the answer to this is. I’d be interested in hearing from people that have implemented this gem, to see how they structure their controller actions.

StatusPage is hiring engineers in San Francisco, Denver and Durham. If you'd like to be part of our incredible team, follow the link, or shoot me an email at tyler at statuspage dot io. We'd would love to chat with you.