Service Objects in Ruby on Rails…and you

rob__race

Rob Race

Posted on February 28, 2017

Service Objects in Ruby on Rails…and you

Note: This tutorial is an excerpt for the service object chapter in my upcoming book Building a SaaS Ruby on Rails 5. The book will guide you from humble beginnings through deploying an app to production. If you find this type of content valuable, the book is on pre-sale right now!


Where do I put this stuff?

We have all been there when you see your controller action getting way too long and hold too much business logic. You know you need to email the user, adjust an account, maybe submit to Stripe and finally ping a Slack Webhook. Well, where should it go? This code needs to exist and doesn't seem to fit in the model. This my friend, is where Service Objects come in!

What's so great about a Service Object? I find in most projects there is nothing easier to reason about or test than a POO(Plain Old Object), which in our case is a PORO(Plain Old Ruby Object). Meaning, the Service Object will be a stand alone class the is not inheriting or extending any other classes from Ruby or Rails.

Let's begin!

Rails does not create a services folder by default so it is our job to create one. A little side-note though, depending on the size of your app or the complexity of business log you may have the need to break services down even further by domain, type or other qualifiers. For the purpose of this post, you can just create a services folder in your app folder mkdir app/services

Restart your rails server to pick up the new folder as Rails will autoload directories in the app directory. Now we can create a file for a user registration service new_registration_service.rb and fill it with our business logic.

class NewRegistrationService  
  def initialize(params)  
    @user = params[:user]  
    @organization = params[:organization]  
  end

  def perform  
    organization_create  
    send_welcome_email  
    notify_slack  
  end

private

def organization_create  
    post_organization_setup if @organization.save  
end

  def post_organization_setup  
    @user.organization_id = @organization.id  
    @user.save  
    @user.add_role :admin, @organization  
  end

  def send_welcome_email  
    WelcomeEmailMailer.welcome_email(@user).deliver_later  
  end

  def notify_slack  
    notifier = Slack::Notifier.new "https://hooks.slack.com/services/89ypfhuiwquhfwfwef908wefoij"  
    notifier.ping "A New User has appeared! #{@organization.name} -   #{@user.name} || ENV: #{Rails.env}"  
  end
end
Enter fullscreen mode Exit fullscreen mode

Ok! Lets go over the Service Object section by section!

class NewRegistrationService  
  def initialize(params)  
    @user = params[:user]  
    @organization = params[:organization]  
  end
Enter fullscreen mode Exit fullscreen mode

Here we are creating the class and then adding the initialize method to create the needed instance variables upon the object being instantiated. In this case, we will pass the object the User model record object and the Organization record object previously created in a new user signup controller.

def perform  
    organization_create  
    send_welcome_email  
    notify_slack  
  end
Enter fullscreen mode Exit fullscreen mode

Personally, I like to wrap the functionality of the whole set of business login in a perform method. I have also seen some proponents of calling different methods manually from the controller as well. It's really a matter of your preference.

def organization_create  
    post_organization_setup if @organization.save  
end

def post_organization_setup  
    @user.organization_id = @organization.id  
    @user.save  
    @user.add_role :admin, @organization  
 end
Enter fullscreen mode Exit fullscreen mode

Here we are creating the organization(which was previously validated in the controller that instantiated this Service Object) and then making the necessary updates to the user such as adding the organization_id needed for the belongs_to relation and assigning a role for the authorization system that is scoped to the previously created organization.

def send_welcome_email  
    WelcomeEmailMailer.welcome_email(@user).deliver_later  
end
Enter fullscreen mode Exit fullscreen mode

A quick call to a Rail's ActionMailer with the user object…

def notify_slack  
    notifier = Slack::Notifier.new "https://hooks.slack.com/services/89ypfhuiwquhfwfwef908wefoij"  
    notifier.ping "A New User has appeared! #{@organization.name} -   #{@user.name} || ENV: #{Rails.env}"  
end
Enter fullscreen mode Exit fullscreen mode

…and finally notifying the Slack Webhook I use for sign up notifications.

Now, you may be saying to yourself, this makes sense but how and where do you call this Service Object? The following is the code extracted from my Devise Registration controller handling the incoming sign up form and all it's Devise goodness:

NewRegistrationService.new({user: resource, organization: @org}).perform
Enter fullscreen mode Exit fullscreen mode

Some readers may start to question some of the aspects of the Service Object in this post. Service Objects do not have a strong convention that I have seen and I have fit this one to match my needs. Earlier, when I said you can call public methods individually, instead of using a perform method, would allow you to wrap some error handling logic. Such as this:

class NewRegistrationService  
  def initialize(params)  
    @user = params[:user]  
    @organization = params[:organization]  
  end

def organization_create  
  begin  
    post_organization_setup if @organization.save  
  rescue  
    false  
  end  
end

....
Enter fullscreen mode Exit fullscreen mode

..and in the controller, something like:

if NewRegistrationService.new({user: resource, organization: @org}).organization_create  
  **success logic**  
else  
  ** redirect_to last_path, notice: 'Error saving record'  
end
Enter fullscreen mode Exit fullscreen mode

Regardless of your Service Object structure, simply extracting your business logic out of your controllers and into Service Objects will help keep your controllers ‘skinny' and your application's code easy to follow.

Note: I follow up with this post with a refactored version of this Service

💖 💪 🙅 🚩
rob__race
Rob Race

Posted on February 28, 2017

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related