Overriding method calls in Ruby?
Solution 1
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.
Solution 2
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
Solution 3
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).
Solution 4
are you trying to hook an instance method of a class? Then the following snippet might help. It uses RCapture which can be installed via
gem install rcapture
An introductionary article can be found at here
require 'rcapture'
class Test
include RCapture::Interceptable
end
Test.capture_post :class_methods => :items do
puts "items!"
end
Test.items
#=> items!
Solution 5
You can do something like this, you can even put conditions on the method being called or not (I don't think that's to useful, but still you have it just in case).
module MethodInterceptor
def self.included(base)
base.extend(ClassMethods)
base.send(:include, InstanceMethods)
base.class_eval do
# we declare the method_list on the class env
@_instance_method_list = base.instance_methods.inject(Hash.new) do |methods, method_name|
# we undef all methods
if !%w(__send__ __id__ method_missing class).include?(method_name)
methods[method_name.to_sym] = base.instance_method(method_name)
base.send(:undef_method, method_name)
end
methods
end
end
end
module ClassMethods
def _instance_method_list
@_instance_method_list
end
def method_added(name)
return if [:before_method, :method_missing].include?(name)
_instance_method_list[name] = self.instance_method(name)
self.send(:undef_method, name)
nil
end
end
module InstanceMethods
def before_method(method_name, *args)
# by defaults it always will be called
true
end
def method_missing(name, *args)
if self.class._instance_method_list.key?(name)
if before_method(name, *args)
self.class._instance_method_list[name].bind(self).call(*args)
else
super
end
else
super
end
end
end
end
class Say
include MethodInterceptor
def before_method(method_name, *args)
# you cannot say hello world!
return !(method_name == :say && args[0] == 'hello world')
end
def say(msg)
puts msg
end
end
Hope this works.
Alex MacCaw
Updated on June 29, 2022Comments
-
Alex MacCaw almost 2 years
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.