Result-ducks
Why Results are Ducks?
If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.
— Duck test
Results are Ducks in the sense that no matter whether you have success
, failure
, or error
- all of them have the same interface with reasonable defaults.
Here is a comparison table:
data | message | code | |
---|---|---|---|
success | *data or {} | *message or "" | *code or :default_success |
failure | *data or {} | *message or "" | *code or :default_failure |
error | *data or {} | *message or "" | *code or :default_error |
Where * means user-provided.
As a user, you have the ability to pass custom values for everything, e.g: extraordinary code for success.
We (as library designers) left such a possibility to avoid the Incomplete Library Class code smell.
But to be honest, we don't see any valuable use cases for now.
Actually, Convenient Service utilizes look-alike wrappers for data, message, and code.
{}
, ""
and :symbol
notations are used to not overcomplicate the table with too low-level details.
As a consequence, you can be confident that any (adequate) status check (like success?
) is enough to proceed:
def show
# ...
# Status is logically checked somewhere.
result.success?
# n lines of code later...
# Won't crash since all `success`, `failure` and `error` respond to `data` and have reasonable defaults.
result.data
# Won't crash since all `success`, `failure` and `error` respond to `message` and have reasonable defaults.
result.message
# Won't crash since all `success`, `failure` and `error` respond to `code` and have reasonable defaults.
result.code
# ...
end
When you forget to check the result status before accessing any of its attributes - an exception is raised.
That is intentional in order to motivate a user to think not only about a happy path but about negative cases as well.
For example:
delivery_result = OrderFoodDelivery.result
# Oh, raises `... Result::Errors::StatusIsNotChecked` since (as its name states) status is not checked.
# How can you eat something if you don't even know whether it is already delivered or not 🙂?
client.eat(delivery_result.data[:food])