Я только что пришел с этим:
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
Используйте его так:
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
после определения метода, моя версия работает только, если вы звоните ему перед тем. Тем не менее, тривиально легко расширить, чтобы он не страдал от этой проблемы.
Заметим также, что я сделал некоторые дополнительные изменения, которые не имеют никакого отношения к этой проблеме, но я думаю, что более Rubyish:
- использовать подмешать вместо класса: наследование является очень ценным ресурсом в Ruby, потому что вы можете наследовать только один класс. Микшины, однако, дешевы: вы можете смешивать столько, сколько хотите. Кроме того: можете ли вы действительно сказать, что домашнее задание IS-A MethodInterception?
Module#define_method
вместо eval
: eval
is evil. 'Достаточно. (Не было абсолютно никакой причины использовать eval
, в первую очередь, в коде OP.)
- использовать метод обертывания метода вместо
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
Это просто и красиво. – Swanand
Одно примечание: alias_method загрязняет пространство имен, но использование alias_method + send приведет к более быстрому выполнению, чем получение ссылки на метод (примерно на 50% быстрее в моем тесте). –