2010-09-23 24 views
5

quiero para interceptar llamadas de método en una clase de rubí y ser capaz de hacer algo antes y después de la ejecución real del método. Probé el siguiente código, pero obtengo el error:Rubí método de intercepción

MethodInterception.rb:16:in before_filter': (eval):2:in alias_method': undefined method say_hello' for class HomeWork' (NameError) from (eval):2:in `before_filter'

¿Alguien me puede ayudar a hacerlo bien?

class MethodInterception 

    def self.before_filter(method) 
    puts "before filter called" 
    method = method.to_s 
    eval_string = " 
     alias_method :old_#{method}, :#{method} 

     def #{method}(*args) 
     puts 'going to call former method' 
     old_#{method}(*args) 
     puts 'former method called' 
     end 
    " 
    puts "going to call #{eval_string}" 
    eval(eval_string) 
    puts "return" 
    end 
end 

class HomeWork < MethodInterception 
    before_filter(:say_hello) 

    def say_hello 
    puts "say hello" 
    end 

end 

Respuesta

2

Menos código fue cambiado del original. Modifiqué solo 2 líneas.

class MethodInterception 

    def self.before_filter(method) 
    puts "before filter called" 
    method = method.to_s 
    eval_string = " 
     alias_method :old_#{method}, :#{method} 

     def #{method}(*args) 
     puts 'going to call former method' 
     old_#{method}(*args) 
     puts 'former method called' 
     end 
    " 
    puts "going to call #{eval_string}" 
    class_eval(eval_string) # <= modified 
    puts "return" 
    end 
end 

class HomeWork < MethodInterception 

    def say_hello 
    puts "say hello" 
    end 

    before_filter(:say_hello) # <= change the called order 
end 

Esto funciona bien.

HomeWork.new.say_hello 
#=> going to call former method 
#=> say hello 
#=> former method called 
14

yo acabamos con esto:

module MethodInterception 
    def method_added(meth) 
    return unless (@intercepted_methods ||= []).include?(meth) && [email protected] 

    @recursing = true # protect against infinite recursion 

    old_meth = instance_method(meth) 
    define_method(meth) do |*args, &block| 
     puts 'before' 
     old_meth.bind(self).call(*args, &block) 
     puts 'after' 
    end 

    @recursing = nil 
    end 

    def before_filter(meth) 
    (@intercepted_methods ||= []) << meth 
    end 
end 

Se usa así:

class HomeWork 
    extend MethodInterception 

    before_filter(:say_hello) 

    def say_hello 
    puts "say hello" 
    end 
end 

Obras:

HomeWork.new.say_hello 
# before 
# say hello 
# after 

El problema básico en el código era que renombró el método en su before_filter método, pero luego en su código de cliente, llamó al before_filter antes de que el método realmente se definiera, lo que resultó en un intento de cambiar el nombre de un método que no existe.

La solución es simple: no hacen eso ™!

Bueno, bueno, tal vez no es tan simple. Usted podría simplemente obligar a sus clientes a llamar siempre before_filter después de que han definido sus métodos. Sin embargo, ese es un mal diseño de API.

Por lo tanto, usted tiene que arreglar de alguna manera para obtener el código de diferir la envoltura del método hasta que realmente existe. Y eso es lo que hice: en lugar de redefinir el método dentro del método before_filter, solo grabo el hecho de que será redefinido más adelante. Luego, realizo la redefinición real en el gancho method_added.

Hay un pequeño problema en esto, porque si agrega un método dentro de method_added, entonces, por supuesto, inmediatamente se volverá a llamar y se volverá a agregar el método, lo que hará que se llame nuevamente, y así sucesivamente. Entonces, necesito protegerme contra la recursión.

Tenga en cuenta que esta solución realidad también impone un orden en el cliente: mientras que la versión de la OP sólo obras si se llama before_filterdespués definir el método, mi versión sólo funciona si usted lo llama antes. Sin embargo, es trivialmente fácil de extender para que no sufra ese problema.

Tenga en cuenta también que hizo algunos cambios adicionales que no están relacionados con el problema, pero que creo que son más Rubyish:

  • utilizar un mixin en lugar de una clase: la herencia es un recurso muy valioso en Rubí, porque solo puedes heredar de una clase. Mixins, sin embargo, son baratos: puedes mezclar tantos como quieras. Además: ¿puedes decir realmente que la tarea IS-A MethodInterception?
  • uso Module#define_method en lugar de eval: eval es malo. 'Dijo Nuff. (No había absolutamente ninguna razón para usar eval en primer lugar, en el código del OP.)
  • utilice la técnica de envoltura de métodos en lugar de alias_method: la técnica de cadena alias_method contamina el espacio de nombres con métodos inútiles old_foo y old_bar. Me gustan mis espacios de nombres limpios.

que acaba de arreglar algunas de las limitaciones que he mencionado anteriormente, y ha añadido algunas características más, pero da pereza volver a escribir mis explicaciones, por lo que volver a publicar la versión modificada aquí:

module MethodInterception 
    def before_filter(*meths) 
    return @wrap_next_method = true if meths.empty? 
    meths.delete_if {|meth| wrap(meth) if method_defined?(meth) } 
    @intercepted_methods += meths 
    end 

    private 

    def wrap(meth) 
    old_meth = instance_method(meth) 
    define_method(meth) do |*args, &block| 
     puts 'before' 
     old_meth.bind(self).(*args, &block) 
     puts 'after' 
    end 
    end 

    def method_added(meth) 
    return super unless @intercepted_methods.include?(meth) || @wrap_next_method 
    return super if @recursing == meth 

    @recursing = meth # protect against infinite recursion 
    wrap(meth) 
    @recursing = nil 
    @wrap_next_method = false 

    super 
    end 

    def self.extended(klass) 
    klass.instance_variable_set(:@intercepted_methods, []) 
    klass.instance_variable_set(:@recursing, false) 
    klass.instance_variable_set(:@wrap_next_method, false) 
    end 
end 

class HomeWork 
    extend MethodInterception 

    def say_hello 
    puts 'say hello' 
    end 

    before_filter(:say_hello, :say_goodbye) 

    def say_goodbye 
    puts 'say goodbye' 
    end 

    before_filter 
    def say_ahh 
    puts 'ahh' 
    end 
end 

(h = HomeWork.new).say_hello 
h.say_goodbye 
h.say_ahh 
+0

Esto es simple e ingenioso. – Swanand

+0

Una nota: alias_method contamina el espacio de nombres, pero usar alias_method + send dará como resultado una ejecución más rápida que obtener una referencia al método (aproximadamente un 50% más rápido en mi prueba). –

0

La solución de Jörg W Mittag es bastante agradable. Si desea algo más sólido (lea bien probado), el mejor recurso sería el módulo de devolución de llamadas de los raíles.

+1

¿dijo él que estaba usando rieles? – horseyguy

+0

Cuento menos de 50 líneas de código en el ejemplo de Jörg (clase de tarea incluida). Seguramente podemos idear una estrategia para probarla hasta que la consideremos robusta y bien probada. –

+0

@banister: No sé de dónde sacó Swanand esa loca noción. Solo el 98% de la gente de ruby ​​usa Rails. –

Cuestiones relacionadas