Forget conditionals, use the Rail way

benjamin roth
Ruby Inside
Published in
3 min readSep 11, 2017

Most of the time, with growing uncertainty, code goes rightwards. Nested conditions are difficult to read and error prone. The real intent is to check success/failure path, so why not make it clear?

Here is an example:

if user_allowed?
if user.save
if profile.save
render_success
else
render_errors(profile.errors)
end
else
render_errors(user.errors)
end
else
render_errors(errors)
end

Or if you use exceptions, it could be:

begin
raise StandardError unless user_allowed?
user.save!
profile.save!
render_success
rescue StandardError => e
# retrieving formatted errors is tricky there.
# maybe with conditionals?
render_errors
end

Of course, you could argue quick returns could do:

return render_errors(errors)         unless user_allowed?
return render_errors(user.errors) unless user.save
return render_errors(profile.errors) unless profile.save?
render_success

But we can still do better.

Rail way programming (yeah, not Rails way) is about making success and error paths obvious in your code. This is the intent of the Waterfall gem.

There is one flow, it goes on or is dammed:

  • once dammed, flow is on the error track
  • as long as you stay in the success track

This is represented like this:

The example above could become:

Flow.new.tap do |flow|
flow
.chain { flow.dam(errors) unless user_allowed? }
.chain { flow.dam(user.errors) unless user.save }
.chain { flow.dam(profile.errors) unless profile.save }
.chain { render_success }
.on_dam { render_errors(flow.error_pool) }
end

Once dammed, only on_dam blocks are executed in a flow.
Because boolean checks are so common, there is syntactic sugar:

Flow.new
.when_falsy { user_allowed? }.dam { errors }
.when_falsy { user.save }.dam { user.errors }
.when_falsy { profile.save }.dam { profile.errors }
.chain { render_success }
.on_dam {|error_pool| render_errors(error_pool) }

Where flows really shine is whenever you have to chain logical blocks together. You can indeed chain one flow with another flow, this is where it gets particularly interesting.

Flow.new
.chain do
Flow.new
.chain { ... }
.chain { ... }
end
.chain { ... }
.on_dam { ... }

Would be cool to:

  • pass data from one flow to the other
  • wrap logic in relevant classes
  • have strategies to rollback on error

Well it is definitely possible!
If you want to see more, check Chain Service Objects like a boss.

You can also check my upcoming book:

As reported, nor the original principle, nor the illustration are from me directly. Both come from https://fsharpforfunandprofit.com/rop/ and they were kind enough to let me reuse the illustrations ;)

--

--

benjamin roth
Ruby Inside

Ru(g)by fan, Ruby on Rails / Javascript freelancer, Haskell lover