There is a version with refactoring example here
Every Lego is an assembly of bricks.
Every piece of software is an assembly of parts.
The Service Object pattern lets you define classes which represent the parts of your business logic.
By convention each Service Object is named out of a verb: it's an action.
Service Objects are bricks.
It's a simple Ruby class.
It must have a precise, action name (LaunchProject, CreateContact)
It's scope is usually limited: specialised object, reusable and easy to test.
You want to process an order.
Steps are:
- remove line items from stock
- complete order
- prepare for shipment
=> Service Objects represent business logic
Service Objects are:
- UpdateStock
- CompleteOrder
- PrepareForShipment
You might wonder what is the point to create an object like CompleteOrder.
This object has something models lack: Context.
No more (conditional) callbacks.
All logic gathered in its own (relevant!) box.
No more fat models nor controllers.
class ProcessOrder < Struct.new(:order)
class RollbackTriggerError < StandardError; end
def call
ActiveRecord::Base.transaction do
update_stock = UpdateStock.new(order).call
raise_error unless update_stock.success?
complete_order = CompleteOrder.new(order).call
raise_error unless complete_order.success?
prepare_for_shipment = PrepareForShipment.new(order).call
raise_error unless prepare_for_shipment.success?
end
rescue RollbackTriggerError
@success = false
ensure
self
end
def success?; @success != false; end
def raise_error; raise RollbackTriggerError; end
end
Process
UpdateStock
CompleteOrder
PrepareForShipment
Unix has pipes
Elixir has pipes and streams
What Ruby has to offer?
I've created Waterfall, a dedicated gem, because I needed this feature.
It keeps on chaining service objects when everything goes well.
It stops the assembly if something goes wrong.
class ProcessOrder < Struct.new(:order)
include Waterfall
class RollbackTriggerError < StandardError; end
def call
ActiveRecord::Base.transaction do
self
.chain { UpdateStock.new(order) }
.chain { CompleteOrder.new(order) }
.chain { PrepareForShipment.new(order) }
.on_dam { raise RollbackTriggerError }
end
rescue RollbackTriggerError
self
end
end
# we assume all services include Waterfall
.chain(:foo) {
# add `foo` to local outflow, which value will
# be what the block returns
}
.chain(bar: :baz) {
# block returns a waterfall which exposes `baz`
# and we rename it `bar` in the local outflow
}
.chain { |outflow|
# outflow is a mere OpenStruct
# in our example, we could use outflow.foo and outflow.bar
}
.when_falsy { some_expression }
.dam {
# you can dam with anything you want, but let it be explanatory
}
.when_truthy { some_other_expression }
.dam {}
.on_dam {|error_pool|
# the error pool contains the value returned by a `dam` in the chain
# or even by any dam from the chained waterfalls
}
Check the github repository.
Contains full api details, examples of code, examples of how to spec.
Ping me @apneadiving