Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding method calls in Ruby?

I'm trying to get a callback when any method on a particular class is called. Overriding "send" doesn't work. It seems send doesn't get called in normal Ruby method invocation. Take the following example.

class Test
  def self.items
   @items ||= []
  end
end

If we override send on Test, and then call Test.items, send doesn't get called.

Is what I'm trying to do possible?

I'd rather not use set_trace_func, since it'll probably slow down things considerably.

like image 200
Alex MacCaw Avatar asked Feb 03 '10 16:02

Alex MacCaw


3 Answers

Use alias or alias_method:

# the current implementation of Test, defined by someone else
# and for that reason we might not be able to change it directly
class Test
  def self.items
    @items ||= []
  end
end

# we open the class again, probably in a completely different
# file from the definition above
class Test
  # open up the metaclass, methods defined within this block become
  # class methods, just as if we had defined them with "def self.my_method"
  class << self
    # alias the old method as "old_items"
    alias_method :old_items, :items
    # redeclare the method -- this replaces the old items method,
    # but that's ok since it is still available under it's alias "old_items"
    def items
      # do whatever you want
      puts "items was called!"
      # then call the old implementation (make sure to call it last if you rely
      # on its return value)
      old_items
    end
  end
end

I rewrote your code using the class << self syntax to open up the metaclass, because I'm not sure how to use alias_method on class methods otherwise.

like image 110
Theo Avatar answered Oct 07 '22 13:10

Theo


Something like this: works with instance methods and class methods, it will not only intercept the current methods defined in the class but any that are added later though reopening the class etc.

(there is also rcapture http://code.google.com/p/rcapture/):

module Interceptor
  def intercept_callback(&block)
    @callback = block
    @old_methods = {}
  end
  def method_added(my_method)
    redefine self, self, my_method, instance_method(my_method)
  end
  def singleton_method_added(my_method)
    meta = class << self; self; end
    redefine self, meta, my_method, method(my_method)
  end
  def redefine(klass, me, method_name, my_method)
    return unless @old_methods and not @old_methods.include? method_name
    @old_methods[method_name] = my_method
    me.send :define_method, method_name do |*args|
      callback = klass.instance_variable_get :@callback
      orig_method = klass.instance_variable_get(:@old_methods)[method_name]
      callback.call *args if callback
      orig_method = orig_method.bind self if orig_method.is_a? UnboundMethod
      orig_method.call *args
    end
  end
end

class Test
  extend Interceptor
  intercept_callback do |*args|
    puts 'was called'
  end
  def self.items
    puts "items"
  end
  def apple
    puts "apples"
  end
end

class Test
  def rock
    puts "rock"
  end
end

Test.items
Test.new.apple
Test.new.rock
like image 43
nightshade427 Avatar answered Oct 07 '22 11:10

nightshade427


You can see how this is done via the ExtLib hook functionality. ExtLib::Hook basically allows you to invoke arbitrary callbacks before or after a method is completed. See the code on GitHub here for how its done (it overrides :method_added to automagically rewrite methods as they're added to the class).

like image 25
Michael Bleigh Avatar answered Oct 07 '22 11:10

Michael Bleigh