Skip to content

Instantly share code, notes, and snippets.

@pcreux
Last active August 21, 2022 15:32
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pcreux/9277929 to your computer and use it in GitHub Desktop.
Save pcreux/9277929 to your computer and use it in GitHub Desktop.
Gourmet Service Objects - Lightning Talk - http://vanruby.org - Feb 27, 2014

Gourmet Service objects

 @pcreux

 Feb 27, 2014
 http://vanruby.org
_______ _______ _________ _
( ____ ) ( ___ ) \__ __/ ( ( /|
| ( )| | ( ) | ) ( | \ ( |
| (____)| | (___) | | | | \ | |
| _____) | ___ | | | | (\ \) |
| ( | ( ) | | | | | \ |
| ) | ) ( | ___) (___ | ) \ |
|/ |/ \| \_______/ |/ )_)

PAIN

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

Service Object

  • A service performs an action

  • Services hold the Biz logic

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

# 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
# A service with an ugly name
class InviteAccepter
def self.accept(invite, user)
# ...
end
end
# An Accept service that accepts
class AcceptInvite
def self.accept(invite, user)
# ...
end
end
# Use generic method!
class AcceptInvite
def self.call(invite, user)
# ...
end
end
# A service can be a Proc!
AcceptInvite = Proc.new(invite, user) { # ... }
AcceptInvite = lambda { |invite, user| # ... }
AcceptInvite = ->(invite, user) { # ... }
# 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
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
# 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
# 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
# Reuse in another controller
class API::InviteController < APIController
def accept
AcceptInvite.call(#...)
end
end
# Use by another service
class BatchAcceptInvite
# ...
def call
@invites.each do |invite|
AcceptInvite.call(invite, user)
end
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 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 from the console!
$> AcceptInvite.call(missed_invite, angry_customer)

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 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment