Code Ruby like you build Lego
There is a version with refactoring example here
What do I mean?
Every Lego is an assembly of bricks.
Every piece of software is an assembly of parts.
Ruby Bricks?
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.
What is a service object?
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.
Example
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
Overengineering?
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.
Assemble Service Objects?
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
this feels like
Our Goal
Process
UpdateStock
CompleteOrder
PrepareForShipment
How to elegantly assemble?
Unix has pipes
Elixir has pipes and streams
What Ruby has to offer?
Assemble with Ruby
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.
Assemble with Waterfall
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
Chaining API
.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
}
Want to know more?
Check the github repository.
Contains full api details, examples of code, examples of how to spec.
Ping me @apneadiving
Happy Building!
code ruby like you build lego
By Benjamin Roth
code ruby like you build lego
Split your app based on business logic bricks and assemble them
- 9,126