Gourmet Service objects
@pcreux
Feb 27, 2014
http://vanruby.org
@pcreux
Feb 27, 2014
http://vanruby.org
_______ _______ _________ _ | |
( ____ ) ( ___ ) \__ __/ ( ( /| | |
| ( )| | ( ) | ) ( | \ ( | | |
| (____)| | (___) | | | | \ | | | |
| _____) | ___ | | | | (\ \) | | |
| ( | ( ) | | | | | \ | | |
| ) | ) ( | ___) (___ | ) \ | | |
|/ |/ \| \_______/ |/ )_) | |
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 |
RE-USE! |
# 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) |
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
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