neumachen
8/28/2014 - 9:16 AM

0-gourmet-service-objects.md

Use service objects!

  • Easy to test Plain Old Ruby Object

  • Reusable from controllers, rake tasks, console, test setup, other services

  • Show what your app DOES app/services/accept_invite.rb app/services/cancel_contract.rb app/services/make_money.rb app/services/send_reminders.rb

Use service objects!

  • TO: remove biz logic from Controllers & Models

  • Because controller should only deal with handling a request (params, cookie, session) and render / redirect.

  • Because AR models should only deal with persistence, associations, validations and scopes

# Use from the console!
$> AcceptInvite.call(missed_invite, angry_customer)
# Use from a rake task!
desc "Accept all invites"
task :wtf do
  Invite.pending.find_each do |invite|
    AcceptInvite.call(invite, User.admin.first)
  end
end
# Use a service to setup test env
Given 'the last invite was accepted by "$name"' do |name|
  AcceptInvite.call(Invite.last, User.find_by_name!(name))
end
# Use by another service
class BatchAcceptInvite
  # ...

  def call
    @invites.each do |invite|
      AcceptInvite.call(invite, user)
    end
  end

end
# Reuse in another controller
class API::InviteController < APIController
  def accept
    AcceptInvite.call(#...)
  end
end
# Experiment: Mind blown...
class InviteController < LoggedInController
  def accept
    AcceptInvite.call(params[:token], current_user,
      success:    ->(invite) { redirect_to invite.item, notice: "Invite accepted!" },
      not_found:  ->(invite) { redirect_to '/', alert: 'Invite not found' },
      error:      ->(invite) { redirect_to '/', alert: invite.errors.full_sentence },
      already_accepted: ->(invite) { redirect_to invite.item }
    )
  end
end
# Here is how to use it...
class InviteController < LoggedInController
  def accept
    invite = Invite.find_by_token!(params[:token])
    if AcceptInvite.call(invite, current_user))
      redirect_to invite.item, notice: "Invite accepted!"
    else
      redirect_to '/', alert: invite.errors.full_sentence
    end
  end
end
AcceptInvite.call(invite, user, -> { raise "ERROR" })
# A service with dependency injection
class AcceptInvite
  def self.call(*args)
    new(*args).call
  end

  def initialize(invite, user, notifier=AcceptInviteNotifier)
    @invite = invite
    @user = user
    @notifier = notifier
  end

  def call
    return true if invite_already_accepted?

    update_invite and send_notification
  end

  private

  def send_notification
    @notifier.call(invite, user)
  end

  def already_accepted?
    # ...
  end

  def update_invite
    # ...
  end

end
# A service that takes advantage of private methods
class AcceptInvite
  def self.call(*args)
    new(*args).call
  end

  def initialize(invite, user)
    @invite = invite
    @user = user
  end

  def call
    return true if invite_already_accepted?

    update_invite and send_notification
  end

  private

  def send_notification
    # ...
  end

  def already_accepted?
    # ...
  end

  def update_invite
    # ...
  end
end
# A service can be a Proc!

AcceptInvite = Proc.new(invite, user) { # ... }

AcceptInvite = lambda { |invite, user| # ... }

AcceptInvite = ->(invite, user) { # ... }
# Use generic method!
class AcceptInvite
  def self.call(invite, user)
    # ...
  end
end
# An Accept service that accepts
class AcceptInvite
  def self.accept(invite, user)
    # ...
  end
end
# A service with an ugly name
class InviteAccepter
  def self.accept(invite, user)
    # ...
  end
end
# A service responsible for several action
class InviteService
  def self.accept(invite, user)
    # ...
  end

  def self.reject(invite, user)
    # ...
  end

  def self.send(invite, user)
    # ...
  end
end

Service Object

  • A service performs an action

  • Services hold the Biz logic

  • Bye bye complex controllers, fat models, callbacks, etc.

PAIN

  • Fat controllers (hard to test)
  • Fat models (hard to test)
  • Slow tests (integration tests)
  • Code duplication
  • WTF is going on?!


         _______    _______    _________    _
        (  ____ )  (  ___  )   \__   __/   ( (    /|
        | (    )|  | (   ) |      ) (      |  \  ( |
        | (____)|  | (___) |      | |      |   \ | |
        |  _____)  |  ___  |      | |      | (\ \) |
        | (        | (   ) |      | |      | | \   |
        | )        | )   ( |   ___) (___   | )  \  |
        |/         |/     \|   \_______/   |/    )_)



Gourmet Service objects

 @pcreux

 Feb 27, 2014
 http://vanruby.org