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 ca开发者_开发知识库ll 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.
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.
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
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).
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.
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!
This does what you want, RCapture: http://cheind.wordpress.com/2010/01/07/introducing-rcapture/
I don't have a complete answer, but I'm thinking method_added might be helpful here.
I've got it working using a Proxy class - and then setting a constant using the real class's name. I'm not sure how to get it working with instances though. Is there a way of changing which object variables are pointing too?
Basically, I want to do this:
t = Test.new
Persist.new(t)
t.foo # invokes callback
Here's the code I used to get it working with classes:
class Persist
class Proxy
instance_methods.each { |m|
undef_method m unless m =~ /(^__|^send$|^object_id$)/
}
def initialize(object)
@_persist = object
end
protected
def method_missing(sym, *args)
puts "Called #{sym}"
@_persist.send(sym, *args)
end
end
attr_reader :object, :proxy
def initialize(object)
@object = object
@proxy = Proxy.new(@object)
if object.respond_to?(:name)
silence_warnings do
Object.const_set(@object.name, @proxy)
end
end
end
end
My approach to this would be to wrap the object I'm trying to log with a Logger shell object which simply calls back to the original object. The code below works by wrapping the object you want to log with a class that simply calls whatever methods you want on the underlying object, but provides a way to trap those calls and log (or whatever) every access event..
class Test
def self.items
puts " Class Items run"
"Return"
end
def item
puts " Instance item run"
return 47, 11
end
end
class GenericLogger
@@klass = Object # put the class you want to log into @@klass in a sub-class
def initialize(*args)
@instance = @@klass.new(*args)
end
def self.method_missing(meth, *args, &block)
retval = handle_missing(@@klass, meth, *args, &block)
if !retval[0]
super
end
retval[1]
end
def method_missing(meth, *args, &block)
retval = self.class.handle_missing(@instance, meth, *args, &block)
if !retval[0]
super
end
retval[1]
end
def self.handle_missing(obj, meth, *args, &block)
retval = nil
if obj.respond_to?(meth.to_s)
# PUT YOUR LOGGING CODE HERE
if obj.class.name == "Class"
puts "Logger code run for #{obj.name}.#{meth.to_s}"
else
puts "Logger code run for instance of #{obj.class.name}.#{meth.to_s}"
end
retval = obj.send(meth, *args)
return true, retval
else
return false, retval
end
end
end
# When you want to log a class, create one of these sub-classes
# and place the correct class you are logging in @@klass
class TestLogger < GenericLogger
@@klass = Test
end
retval = TestLogger.items
puts "Correctly handles return values: #{retval}"
tl = TestLogger.new
retval = tl.item
puts "Correctly handles return values: #{retval}"
begin
tl.itemfoo
rescue NoMethodError => e
puts "Correctly fails with unknown methods for instance of Test:"
puts e.message
end
begin
TestLogger.itemsfoo
rescue NoMethodError => e
puts "Correctly fails with unknown methods for class Test"
puts e.message
end
Output from that code sample is:
Logger code run for Test.items
Class Items run
Correctly handles return values: Return
Logger code run for instance of Test.item
Instance item run
Correctly handles return values: [47, 11]
Correctly fails with unknown methods for instance of Test:
undefined method `itemfoo' for #<TestLogger:0x2962038 @instance=#<Test:0x2962008>>
Correctly fails with unknown methods for class Test
undefined method `itemsfoo' for TestLogger:Class
singleton_method_added
can give you a simple solution:
class Item
@added_methods = []
class << self
def singleton_method_added name
if name != :singleton_method_added && !@added_methods.include?(name)
@added_methods << name
pMethod = self.singleton_method name
self.singleton_class.send :define_method, name do |*args, &blk|
puts "Callback functions calling..."
pMethod.call(*args, &blk)
end
end
end
def speak
puts "This is #{self}"
end
end
Hope this will help.
精彩评论