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_filter
despué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
Esto es simple e ingenioso. – Swanand
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). –