At Your Service

Service Objects made easy (& works great w/ Rails)

Model / View / Controller isn't enough for today's complex web applications. Encapsulate your business logic in Service objects so that when you write a piece of business logic, you only write it once.

Inspiration & How to Use Service Objects:

Gourmet Service Objects --- by Brewhouse

Using Services to Keep Your Rails Controllers Clean and DRY --- by Engine Yard

7 Patterns to Refactor Fat ActiveRecord Models --- by Code Climate (see #2)

Service objects in Rails will help you design clean and maintainable code. Here's how. --- via netguru

Installation

Add this line to your application's Gemfile:

gem 'at_your_service'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install at_your_service

Example

Adding a user's Credit Card is a common piece of business logic. The below example uses Stripe to do so. By having this logic in a Service, whether it is an admin manually adding a new Credit Card for a User OR the User adding it themselves in the checkout flow, the logic will be exactly the same. Though the Renter object is the primary Active Record model involved, having this method as part of the Renter model would lead to that class being massive and unwieldy. It is much better as a standalone service, as shown below.

class AddCard
  include AtYourService.with(strict: true)

  attribute :renter, Renter
  attribute :token, String
  attribute :agent_name, String

  UPDATE_RENTER_CUSTOMER_ID_ERROR = 'Could not associate Stripe data with Renter'

  def call
    begin
      customer = nil
      cards = []
      if renter.stripe_customer_id
        # Add card to customer
        customer = Stripe::Customer.retrieve(renter.stripe_customer_id)
        cards << customer.sources.create(source: token)
      else
        # Create new customer w/ card
        customer = Stripe::Customer.create(
          source: token,
          email: renter.email,
          description: "#{renter.full_name} - ##{renter.id} - created by #{agent_name}"
        )
        unless customer && renter.update(stripe_customer_id: customer.id)
          Rails.logger.error "New Stripe Customer #{customer.inspect} could not be associated with Renter #{renter.inspect}"
          return Error.new(UPDATE_RENTER_CUSTOMER_ID_ERROR)
        end
      end
      cards += customer.sources.data
      return Success.new(renter: ::Web::RenterStripeDecorator.new(renter), cards: cards)
    rescue Stripe::CardError => e
      return Error.new(e.json_body[:error][:message])
    end
  end
end

Setup

  1. create directory app/services.
  2. Add config.autoload_paths += Dir["#{config.root}/app/services"] to config/application.rb.
  3. That's it! You are now ready to write your first Service

How to Write a Service

  1. Create a class in the app/services directory
  2. Include AtYourService
  3. Define Attributes (strict or lenient) via the Virtus Gem
  4. Create the call method
  5. Write your Business Logic (+ private helper methods)
  6. Return a Success or Error object
Create a class in the app/services directory

Name your class with what the service does. If it's adding a credit card, call it AddCard. If it's generating a quote, call it GenerateQuote, etc.

Include AtYourService

Include the AtYourService module via include AtYourService

Define Attributes

With attributes, we clearly define the data that Service needs including the types. With Strict Coercion mode from Virtus, an error will be thrown if the inputted arguments cannot be coerced into the desired type (so no nils as arguments). By default, strict coercion mode is off and you can have nil attributes. To use strict coercion mode, modify your AtYourService include to be include AtYourService.with(strict: true)

Create the call Method

At Your Service flavored service objects always define call. This prevents the awkwardness of having to name both your service class and it's method (stuff like AddCard.add_card). Now just name your class for what the service does (AddCard, SendInvite, MarkActiveReservation, ReturnProduct etc.) and then you always know you call it with .call.

Write your Business Logic

Inside call, define your business logic. Reference attributes by name, so attribute :user would just be user inside call. Go wild here, Service objects are meant to house the complex business logic of your application. For convenience, you can define private helper methods below call like usual.

Return a Success or Error object

Again, with At Your Service we are all about consistency. Every Service should return either a Success or Error object and pass into it the appropriate data (for Success) or error messages (for Error). Take a look at the source of these two classes. They're very simple. For convenience, Error.new accepts either a string or array of string error messages. With Success you can pass in any data you want, but usually a hash {user: user, order: order} works best.

Rails API Example

Services are a great way to shrink your Rails controllers. Rather than having messy business logic in your controller, just offload it to a reusable Service that can be called across your codebase. I have many 2-line controller methods like so:

class OrderWizardController < ApplicationController
  def generate_quote
    result = GenerateQuote.call(params)
    render json: result.display
  end

  def accept_quote
    result = AcceptQuote.call(params)
    render json: result.display
  end

  def remove_line_item
    result = RemoveLineItem.call(params)
    render json: result.display
  end
  # ... etc ...
end

Questions?

At Your Service is a new gem. If any of the above documentation is confusing, unclear or insufficient please let me know! I am always available via email at [email protected] to help you out :)

Contributing to At Your Service

Running the Tests

$ rspec

Running the Rails Tests

$ cd rails_test/rails_app/

$ rspec

PRs and Issues are always welcome!

License

The gem is available as open source under the terms of the MIT License.