-.- --. .-. --..

Using conditionals outside method definitions inside a class

11 Sep 2013

I was fiddling with some code and remembered I could do this:

class Foo

  def bar
    puts "this is bar"
  end

  alias_method :baz, :bar if self.new.respond_to?(:bar)
end

__END__

irb> Foo.new.baz
"this is bar"

Note the new method on self. self there is the class and not the instance of the class and so, the new method is necessary

I don’t exactly know if this is a good practice or whether this pattern is used elsewhere (read as “in much more successful projects used by bazillion other people”). However, I did use it for a problem I was solving.

Example

The following example illustrates the use-case. A counter program counts the number of times a method Bar#bar is called during the runtime of a program. This program can be as simple as 1000.times { Bar.new.bar } for testing purposes.

Here is a spec for the counter program:

Given a method name like Bar#bar as input
And given a snippet of code that uses the bar method
We want to count the number of instances the method has been called

The example snippet to measure is:

# input.rb

class Bar
  def bar
  end
end

1000.times do
  Bar.new.bar
end

And here is a simple implementation.

# counter.rb

INCREMENT_COUNT = lambda do
  @counter ||= 1
  @counter += 1
end


class Bar

  alias_method :baz, :bar

  def bar
    INCREMENT_COUNT.call
    self.__send__(:baz)
  end
end

at_exit {
  puts "Bar#bar called #{@counter} times"
}
__END__

$ ruby -r ./counter.rb input.rb
Bar#bar called 1000 times

The method that is being overridden should be aliased to avoid infinite recursion. This works fine when the class Bar has the method bar defined in it. What if the method bar is defined inside a module and the module is included in the class Bar?

For example,

# input.rb

module Foo
  def bar
  end
end

class Bar
  include Foo
end

__END__

$ ruby -r ./counter.rb input.rb
NoMethodError #bar for class Bar

If the counter program is unmodified, it fails since the alias_method can’t find the bar method inside the class. So, instead of aliasing, super should be used which sends the bar method to the module Foo which is next in ruby inheritance chain

The modified counter program looks like so:

class Bar
  def bar
    INCREMENT_COUNT.call
    super
  end
end

And to serve both the cases, a naive implementation would be:

# input.rb
module Foo
  def bar
  end
end

class Bar
  include Foo
end

1000.times do
  Bar.new.bar
end

# counter.rb

INCREMENT_COUNT = lambda do
  @counter ||= 1
  @counter += 1
end

class Bar

  alias_method :baz, :bar if self.new.respond_to?(:bar)

  def bar
    INCREMENT_COUNT.call
    begin
      self.__send__(:baz)
    rescue NoMethodError
      super
    end
  end
end

at_exit {
  puts "Bar#bar called #{@counter} times"
}

__END__

$ ruby -r ./counter.rb input.rb
Bar#bar called 1000 times

Avoid using exceptions for program flow, a wise man once said. I hope he has no internet.