Переопределение вызовов методов в Ruby? - PullRequest
7 голосов
/ 03 февраля 2010

Я пытаюсь получить обратный вызов, когда вызывается любой метод определенного класса. Переопределение «отправить» не работает. Кажется, что send не вызывается при обычном вызове метода Ruby. Возьмите следующий пример.

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

Если переопределить отправку в Test, а затем вызвать Test.items, send не будет вызван.

Возможно ли то, что я пытаюсь сделать?

Я бы предпочел не использовать set_trace_func, поскольку это, вероятно, значительно замедлит работу.

Ответы [ 10 ]

12 голосов
/ 03 февраля 2010

Используйте alias или 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

Я переписал ваш код, используя синтаксис class << self, чтобы открыть метакласс, потому что я не уверен, как иначе использовать alias_method в методах класса.

4 голосов
/ 03 февраля 2010

Примерно так: работает с методами экземпляра и методами класса, он будет перехватывать не только текущие методы, определенные в классе, но и все методы, добавленные позже при повторном открытии класса и т. Д.

(есть также 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
2 голосов
/ 03 февраля 2010

Вы можете увидеть, как это делается с помощью функции ловушки ExtLib. ExtLib :: Hook в основном позволяет вызывать произвольные обратные вызовы до или после завершения метода. Посмотрите код на GitHub здесь, чтобы узнать, как это делается (он переопределяет :method_added для автоматического перезаписи методов по мере их добавления в класс).

1 голос
/ 04 февраля 2010

вы пытаетесь подключить метод экземпляра класса?Тогда следующий фрагмент может помочь.Он использует RCapture, который может быть установлен через

gem install rcapture

Вводную статью можно найти по адресу здесь

require 'rcapture'

class Test 
  include RCapture::Interceptable
end

Test.capture_post :class_methods => :items do
  puts "items!"
end

Test.items 
#=> items!
1 голос
/ 03 февраля 2010

Вы можете сделать что-то подобное, вы даже можете поставить условия для вызываемого метода или нет (я не думаю, что это полезно, но все же у вас это есть на всякий случай).

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

Надеюсь, это сработает.

0 голосов
/ 25 ноября 2016

singleton_method_added может дать вам простое решение:

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

Надеюсь, это поможет.

0 голосов
/ 26 сентября 2013

Мой подход к этому состоит в том, чтобы обернуть объект, который я пытаюсь записать, с помощью объекта оболочки Logger, который просто вызывает исходный объект. Приведенный ниже код работает, оборачивая объект, который вы хотите зарегистрировать, с классом, который просто вызывает любые методы, которые вы хотите, для базового объекта, но обеспечивает способ перехватывать эти вызовы и регистрировать (или что-то еще) каждое событие доступа.

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

Выходные данные из этого примера кода:

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
0 голосов
/ 03 февраля 2010

У меня все работает, используя класс Proxy - и затем устанавливаю константу, используя имя реального класса Я не уверен, как заставить это работать с экземплярами все же. Есть ли способ изменить, какие переменные объекта тоже указывают?

В принципе, я хочу сделать это:

t = Test.new
Persist.new(t)

t.foo # invokes callback

Вот код, который я использовал для работы с классами:

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
0 голосов
/ 03 февраля 2010

У меня нет полного ответа, но я думаю, method_added может быть полезным здесь.

0 голосов
/ 03 февраля 2010

Это делает то, что вы хотите, RCapture: http://cheind.wordpress.com/2010/01/07/introducing-rcapture/

...