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

  • 8,861