Sonntag, 5. Oktober 2014

Remove boolean parameters from your Ruby API!

Boolean parameters in method definitions are not a good choice for various reasons. They:
  1. are not readable
  2. duplicate code or responsibility inside and outside the method
There are alternatives to expecting true/ false. Let us assume a Ruby class like:
class Food
  def self.order vegetarian=false
    return "Vegetarian food" if vegetarian
    "Conventional food"
  end
end
The original code works so far, but using it is no laughing matter:
Food.order true # => "Vegetarian food"
Food.order # => "Conventional food"
What does "Food.order true" express? Does it mean "The Food order is no lie and is meant seriously"? TrueClass and FalseClass are not expressive in no way.
Even following the expressive boolean pattern (Express boolean parameters the Ruby way!) is not the best choice:
Food.order :vegetarian # => "Vegetarian food"
Food.order # => "Conventional food"
That is a little nicer, but it would be great to make even this work:
Food.order :conventional # => "Conventional food"
Every time the API is yours you have the choice to make it work great.
Use Enums instead of Booleans!
The original class should be refactored and expect Enum like:
class Food
  def self.order type=:conventional
    types = %i(conventional vegetarian)
    fail "Invalid Food type." unless types.member? type
    return "Conventional food" if type.eql? :conventional
    "Vegetarian food"
  end
end
and then:
Food.order :vegetarian # => "Vegetarian food"
Food.order :conventional # => "Conventional food"
Food.order :something_else # => RuntimeError: 
# Invalid Food type.
# Must one be of :conventional or :vegetarian.
The Enum method definition directs to a strict parameter expectation, which prevents API misinterpretations.
Although it still couples code or responsibility from outside and inside the method, there is another pro for Enums. It is way more extensible than boolean parameters can be.
A new requirement wants vegan food to be added:
class Food
  def self.order type=:conventional
    types = %i(conventional vegetarian vegan)
    fail "Invalid Food type." unless types.member? type
    case type
    when :vegetarian
      "Vegetarian food"
    when :vegan 
      "Vegan food"
    else "Conventional food"
    end
  end
end
Forgive the usage of a case statement for simplicity reasons.
Every time a new type of food has to be added the method can be changed without hurting the message senders, whereas boolean parameters will ruin the public API.
Further articles of interest:

Supported by Ruby 2.1.1