2010-09-23 4 views
5

Я хочу перехватить вызовы метода на классе ruby ​​и сделать что-то до и после фактического выполнения метода. Я попробовал следующий код, но получим ошибку:Перехват метода Ruby

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'

Может кто-нибудь помочь мне сделать это правильно?

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 

ответ

2

Меньше код был изменен с оригинала. Я изменил только 2 строки.

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 

Это хорошо работает.

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

Я только что пришел с этим:

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 
+0

Это просто и красиво. – Swanand

+0

Одно примечание: alias_method загрязняет пространство имен, но использование alias_method + send приведет к более быстрому выполнению, чем получение ссылки на метод (примерно на 50% быстрее в моем тесте). –

0

Решение Jörg W Mittag довольно приятно. Если вы хотите что-то более надежное (хорошо прочитанный тест), лучшим ресурсом будет модуль обратных вызовов rails.

+1

он говорил, что использовал рельсы ??! – horseyguy

+0

Я считаю менее 50 строк кода в примере Йерга (класс Homework включен). Конечно, мы можем разработать стратегию тестирования, пока мы не будем считать ее надежной и проверенной. –

+0

@ banner: Не знаю, откуда у Суванда было это сумасшедшее понятие. Только 98% рубинов используют Rails. –

Смежные вопросы