This presentation is based on this article from brewhouse.io.
- László Bácsi, @icanscale, github/lackac
- R3
- 100Starlings
This presentation is based on this article from brewhouse.io.
# a service object (aka method object) performs one action | |
# the simplest service object is a lambda | |
send_notification = lambda do |user, message| | |
DeviceNotifier.new(user).send(message) | |
NotificationMailer.new(user, message).deliver_later | |
end | |
send_notification.call(alice, "Bob says 'Hi!'") |
Use service objects to encapsulate actions one or more of the following traits:
#call
SendNotification
, RefundPayment
)app/services
, use subdirectories and namespaces for
grouping together related servicesThis is one way of writing services. There are a number of other approaches out there, and some styles could be even mixed in the same project for different purposes.
# the same service object but as a class | |
class SendNotification | |
def self.call(user, message) | |
DeviceNotifier.new(user).send(message) | |
NotificationMailer.new(user, message).deliver_later | |
end | |
end |
# things get complicated | |
class MakePayment | |
def self.call(order, gateway) | |
transaction = create_transaction(order) | |
gateway.make_payment(payment_payload(transaction)).tap do |response| | |
log_response(response) | |
end | |
end | |
private | |
def create_transaction(order) | |
order.transactions.create type: :payment, amount: order.amount_due | |
end | |
def payment_payload(transaction) | |
{ | |
merchant_ref: transaction.uuid, | |
# ... | |
} | |
end | |
end |
# let's simplify things | |
module Service | |
extend ActiveSupport::Concern | |
included do | |
def self.call(*args) | |
new(*args).call | |
end | |
end | |
end |
# now that we can use instances, let's simplify initialization | |
# introducing [Virtus][1]: Attributes on Steroids for POROs | |
# | |
# [1]: https://github.com/solnic/virtus | |
class MakePayment | |
include Service | |
include Virtus.model | |
attribute :order, Order | |
attribute :gateway, nil, default: ->(*) { CreateGatewayClient.call } | |
def call | |
create_transaction | |
gateway.make_payment(payment_payload) do |response| | |
log_response(response) | |
end | |
end | |
private | |
attr_accessor :transaction | |
# ... | |
end |
discoverable application – just browse through app/services
self-documenting
clean controllers and models
modular and composable
fast and focused tests
easily reusable, call from anywhere (other service objects, rake tasks, cron jobs, console, test helpers, etc.)
almost functional
There are 3 flavors:
Hash#fetch
, create!
, save!
, etc.success?
, error
, status
, etc.