开发者

Overriding method calls in Ruby?

开发者 https://www.devze.com 2022-12-19 15:09 出处:网络
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

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.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号