Forget conditionals, use the Rail way
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 ;)