Я только что придумал это:
module MethodInterception
def method_added(meth)
return unless (@intercepted_methods ||= []).include?(meth) && !@recursing
@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
Используйте это так:
class HomeWork
extend MethodInterception
before_filter(:say_hello)
def say_hello
puts "say hello"
end
end
Работает:
HomeWork.new.say_hello
# before
# say hello
# after
Основная проблема в вашем кодебыло то, что вы переименовали метод в своем методе before_filter
, но затем в своем клиентском коде вы вызвали before_filter
до того, как метод был фактически определен, что привело к попытке переименовать метод, который не существует.
Решение простое: не делай так ™!
Ну, ладно, может быть, не все так просто.Вы могли бы просто заставить своих клиентов всегда звонить before_filter
после того, как они определили свои методы.Однако это плохой дизайн API.
Итак, вы должны каким-то образом организовать свой код, чтобы отложить перенос метода до его фактического существования.И это то, что я сделал: вместо переопределения метода в методе before_filter
я записываю только тот факт, что он будет переопределен позже.Затем я выполняю фактическое переопределение в хуке method_added
.
В этом есть небольшая проблема, потому что если вы добавляете метод внутри method_added
, то, конечно, этобудет немедленно вызван снова и добавит метод снова, что приведет к его повторному вызову и так далее.Итак, мне нужно защититься от рекурсии.
Обратите внимание, что это решение на самом деле также обеспечивает порядок на клиенте: в то время как версия OP только работает, если вы вызываете before_filter
после определения метода, моя версия работает, только если вы назовете ее до .Тем не менее, его тривиально легко расширить, чтобы он не страдал от этой проблемы.
Обратите также внимание на то, что я внес некоторые дополнительные изменения, которые не связаны с проблемой, но я думаю, что они более рубиновые:
- используйте миксин вместо класса: наследование является очень ценным ресурсом в Ruby, потому что вы можете наследовать только от одного класса.Миксины, однако, дешевы: вы можете смешивать столько, сколько захотите.Кроме того: можете ли вы сказать, что Homework IS-A MethodInterception?
- использовать
Module#define_method
вместо eval
: eval
- зло.'Достаточно.(Не было абсолютно никакой причины вообще использовать eval
в коде ОП.) - использовать метод обтекания метода вместо
alias_method
: метод цепочки alias_method
загрязняет пространство именбесполезные old_foo
и old_bar
методы.Мне нравятся мои чистые пространства имен.
Я только что исправил некоторые из упомянутых выше ограничений и добавил еще несколько функций, но мне лень переписывать мои объяснения, поэтому я публикую измененноеверсия здесь:
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