Sonntag, 18. Mai 2014

Chain your Ruby methods!

Method chaining is a very helpful pattern for every API. In fact it is syntactic sugar, which eliminates the need for intermediate variables. But it requires the method to return something meaningful to chain on (read Return something meaningful!).
The benefits are:
  1. less intermediate variables (and therefore more readable)
  2. predictable results (and therefore a stable API)
Well, this example class in person.rb:
class Person
  def initialize name, gender
    @name = name
    @gender = gender
  end

  def marries person
    @marriage_partner = person
  end

  def bear_child
    return unless @gender.eql? 'f'
    @children ||= 0
    @children += 1
  end
end
can create a new person, who can marry another person and even bear children, if its gender is female. The class can be used like:
@alice = Person.new 'Alice', 'f'
@bob = Person.new 'Bob', 'm'
@alice.bear_child
@alice.marries @bob
The code can be refactored by method chaining into one single line:
Person.new('Alice', 'f').bear_child.marries(Person.new('Bob', 'm'))
=> NoMethodError: undefined method `marries' for 1:Fixnum
which fails, because the method bear_child returns a number (1) and not the expected Person object.
If the order of the chaining is changed:
Person.new('Alice', 'f').marries(Person.new('Bob', 'm')).bear_child
there is no exception, but it is still an unexpected result. It is even worse, because the bug is harder to debug. The reason is: in the example the method marries returns @bob, who obviously can not bear a child.
Well, methods can only be chained perfectly, if they return something meaningful and predictable.
The refactored Person class in person.rb:
class Person
  def initialize name, gender
    @name = name
    @gender = gender
  end

  def marries person
    @marriage_partner = person
    self
  end

  def bear_child
    return self unless @gender.eql? 'f'
    @children ||= 0
    @children += 1
    self
  end
end
Both methods marries and bear_child return self in every case. So that they can be chained together in a cascading style like:
Person.new('Alice', 'f').bear_child.marries(Person.new('Bob', 'm'))
 => #<Person:0x00000001be31f0 @name="Alice", @gender="f", @marriage_partner=#<Person:0x00000001be30b0 @name="Bob", @gender="m">, @children=1>
and it does not matter in which order:
Person.new('Alice', 'f').marries(Person.new('Bob', 'm')).bear_child
 => #<Person:0x00000001be31f0 @name="Alice", @gender="f", @marriage_partner=#<Person:0x00000001be30b0 @name="Bob", @gender="m">, @children=1>
the interim result is always the same expected person and the result state is equal.
Please note, that the meaning of nil is pretty low and even true and false are only meaningful in certain contexts.

Supported by Ruby 2.1.1

Keine Kommentare:

Kommentar veröffentlichen