2010-02-03 16 views
7

Estoy tratando de obtener una devolución de llamada cuando se llama a cualquier método en una clase en particular. Reemplazar "enviar" no funciona. Parece que enviar no se llama en la invocación normal del método Ruby. Toma el siguiente ejemplo.Llamadas de método de anulación en Ruby?

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

Si sobreescribimos enviar en la prueba, y luego llamamos Test.items, envío no se consiga llamar.

¿Es lo que estoy tratando de hacer posible?

Preferiría no usar set_trace_func, ya que probablemente ralentizará considerablemente las cosas.

+1

Puede ser posible hacer esto aliasing la clase a mi propia clase y delegando llamadas de método a la clase original. –

+1

Protip: todas las respuestas que le dicen cómo sobrescribir un solo método también le indican cómo hacerlo a todos los métodos que implementa una clase. Solo necesita iterar sobre ellos. ¿Esa es tu verdadera pregunta? – Chuck

Respuesta

12

Uso alias o 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 

Reescribí el código utilizando la sintaxis class << self para abrir la metaclase, porque no estoy seguro de cómo utilizar alias_method en los métodos de clase de otro modo.

+2

No, quiero una devolución de llamada para * any * invocation de método. Items es solo un ejemplo. –

+0

Así que hazlo con todos tus métodos. – Chuck

+1

Combinado con ': method_added' debe hacer lo que necesita. – Theo

0

no tengo una respuesta completa, pero estoy pensando method_added podría ser útil aquí.

2

Puede ver cómo se hace esto a través de la funcionalidad de enlace ExtLib. ExtLib::Hook básicamente le permite invocar devoluciones de llamada arbitrarias antes o después de que se complete un método. Consulte el código en GitHub aquí para saber cómo se hace (reemplaza :method_added para reescribir automáticamente los métodos a medida que se agregan a la clase).

4

Algo como esto: trabaja con los métodos de instancia y métodos de la clase, no sólo va a interceptar a los actuales métodos definidos en la clase, pero ninguna que se agregan después de que la reapertura de la clase, etc.

(también hay 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 
+0

Aún más útil si pasa 'method_name' y' caller' a la devolución de llamada delante o en lugar de '* args', y luego dice' intercept_callback do | method_name, by_caller | ; pone "# {method_name} llamado por # {by_caller.first}"; fin' –

1

usted puede hacer algo como esto, incluso se puede poner condiciones sobre el método que se llama o no (no creo que es de utilidad, pero aún lo tienes por si acaso).

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 

Espero que esto funcione.

0

Lo tengo trabajando usando una clase Proxy - y luego estableciendo una constante usando el nombre de la clase real. Aunque no estoy seguro de cómo hacerlo funcionar con instancias. ¿Hay alguna forma de cambiar qué variables de objeto apuntan también?

Básicamente, quiero hacer esto:

t = Test.new 
Persist.new(t) 

t.foo # invokes callback 

Aquí está el código utilicé para que funcione con las clases:

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

En realidad, en segundo lugar, la redefinición del método podría ser el camino a seguir; también funcionaría con las instancias: https://gist.github.com/5226decf57adc11baf46 –

1

estás tratando de enganchar un método de instancia de una clase? Entonces el siguiente fragmento podría ayudar.Utiliza RCapture que se puede instalar a través de

gem install rcapture 

Un artículo introductionary se puede encontrar en here

require 'rcapture' 

class Test 
    include RCapture::Interceptable 
end 

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

Test.items 
#=> items! 
+0

del famoso desarrollador de RCapture. – martinus

0

Mi acercamiento a esto sería para envolver el objeto que estoy tratando de conectarse con una cáscara Logger objeto que simplemente llama al objeto original. El código siguiente funciona envolviendo el objeto que desea registrar con una clase que simplemente llama a los métodos que desee en el objeto subyacente, pero proporciona una forma de interceptar esas llamadas y registrar (o lo que sea) cada evento de acceso.

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 

salida de ese ejemplo de código es:

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

singleton_method_added le puede dar una solución sencilla:

class Item 
    @added_methods = [] 
    class << self 
    def singleton_method_added name 
     if name != :singleton_method_added && [email protected]_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 

Esperamos que esto ayude.

Cuestiones relacionadas