Service objects in Ruby and Rails09 Feb 2015 in Ruby and Rails
A common design pattern for performing tasks after an object is created is to use an ActiveModel Callback. For example:
class User < ActiveRecord::Base after_create :send_welcome_email def send_welcome_email # Send an email end end
Yes, this is simplistic, but there are a few problems with this.
- It's not the User models responsibility to send an email.
- Unless it modifies internal state, callbacks should be avoided.
- Testing becomes painful and often times requires stubbing.
Lets talk about responsibility for a moment. In my opinion, if it's an interaction, it shouldn't belong to one specific model. What if you need a send_invoice_email to go with send_welcome_email? This can quickly get out of hand. This is why I use service objects.
So what exactly is a service object? It's really just an object that encapsulates operations. Using our initial callback example, lets refactor it to use a service object by adding the following to app/services/send_welcome_email.rb
class SendWelcomeEmail def self.call(user) UserMailer.welcome_email(user).deliver end end
Now to send a welcome email, you would do:
This makes it far easier to test and decouples the responsibility.
If you've read other articles on service objects, you've probably run into multiple implementation methods. Some developers advocate that a service object should only respond to call, and only perform a single task. I don't see a reason for being so nitpicky. Instead, my service objects encapsulate related responsibility. For example, integrating with a third-party service:
class StripeCustomer def initialize(member) @member = member end # Create a new stripe customer def create end # Update existing stripe customer def update end # Fetch the stripe customer info def fetch end end
This is a much cleaner approach than, say, creating the following:
At the end of the day, simply separating this logic is going to make your life a lot easier.