Service objects 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.

  1. It's not the User models responsibility to send an email.
  2. Unless it modifies internal state, callbacks should be avoided.
  3. 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:

SendWelcomeEmail.call(user)

This makes it far easier to test and decouples the responsibility.

Implementations

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:

  • app/services/stripe/customer_create.rb
  • app/services/stripe/customer_update.rb

At the end of the day, simply separating this logic is going to make your life a lot easier.

Setting default values in ActiveRecord

I constantly find a need for default values in my ActiveRecord models. Many people recommend database migrations for this, but unless it's for counters, I try to keep it application side. A common approach is to use an after_initialize call:

class Account < ActiveRecord::Base
  # After initialization, set default values
  after_initialize :set_default_values
  
  def set_default_values
    # Only set if time_zone IS NOT set
    self.time_zone ||= Time.zone.to_s
  end
end

This is a standard ActiveRecord callback that gets called both when an object is instantiated and when retrieved from the database. Be sure to set the value conditionally, as you don't want to overwrite the value when it's pulled from the database.

I'm sure we can clean this up a little using ActiveSupport::Concern. Concerns are very similar to standard Ruby modules, but with some added (and semi-controversial) functionality. All we need to do is add the following to app/models/concerns/defaults.rb:

module Defaults
  # Added to instance of object
  included do
    after_initialize :apply_default_values
  end

  # Callback for setting default values
  def apply_default_values
    self.class.defaults.each do |attribute, param|
      next unless self.send(attribute).nil?
      value = param.respond_to?(:call) ? param.call(self) : param
      self[attribute] = value
    end
  end

  # Added to class of object
  class_methods do
    def default(attribute, value = nil, &block)
      defaults[attribute] = value
      # Allow the passing of blocks
      defaults[attribute] = block if block_given?
    end

    def defaults
      @defaults ||= {}
    end
  end
end

Including this file does the following:

  1. Adds a default method for assigning mappings
  2. Adds a defaults method for returning mappings
  3. Defines a callback which iterates over the mappings and assigns the default values.

Using this concern is as simple including it into your model and calling default on the attribtues you want to have a default value.

class Account < ActiveRecord::Base
  # Include the concern
  include Defaults
  
  # We can define here
  default :time_zone, Time.zone.to_s
  
  # Or pass a block
  default :time_zone do
    Time.zone.to_s
  end
end

Not only is this simple to implement, utilizing concerns will DRY up your code. Just remember, if you're using a concern in only one model, there really isn't a reason for it.

Decorating with Decco

I'll be honest, I'm not a fan of Rails helpers. There's something about poluting a global namespace with unorganized cruft that makes me twitch ever so slightly. How do we get away, or at least minimize the usage of helpers? Simple, we use a decorator. A decorator allows you to add additional functionality to an object without it deviating away from it's core responsibility. A quick search of Github yields many decorating systems, the most noteable being Draper. I've used Draper and think it's a fantastic library, but for me it feels a bit cumbersome for my needs. This is why I created Decco.

Decco is a combination decorator/presenter system. It's designed to be lightweight and simple to use, built off simple Ruby classes. All you need to do is create a class that takes an object to be decorated when instantied. For example:

class UserDecorator
  def initialize(user, view)
    @user = user
    @view = view
  end
  
  def gravatar_url(options = {})
    hash = Digest::MD5.hexdigest(@user.email)
    "http://www.gravatar.com/avatar/#{hash}?#{options.to_query}"
  end
end

From here you can instantiate a new instance:

# Infers name from object -- UserDecorator
Decco.decorate(@user)

# Specify a decorator
Decco.decorate(@user, OtherUserDecorator)

If you're using this within Rails, then you get an additional helper for your views:

# Get a decorator singleton instance
d(@object)
image_tag(d(@user).gravatar_url)

This helper instanties the decorator with the current view object so you can call Rails helpers within your decorator.

Ultimately, Decco strives to be as simplistic as possible. It's merely a simple wrapper to instantiate your decorator/presenter based on the object as well as provide a simple helper for caching the decorate call.

View Decco on Github