Forget conditionals, use the Rail way

benjamin roth
Sep 11, 2017 · 3 min read

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 ;)

Ruby Inside

Ruby articles and posts

benjamin roth

Written by

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

Ruby Inside

Ruby articles and posts

More From Medium

More from Ruby Inside

More from Ruby Inside

What’s coming to Rails 6.0?

3.2K

More from Ruby Inside

More from Ruby Inside

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade