Рубиновый метод перехвата

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

MethodInterception.rb: 16: вbefore_filter': (eval):2:inalias_method ': неопределенный методsay_hello' for classHomeWork '(NameError) из (eval): 2: в `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

Ответы на вопрос(3)

дежное (читай хорошо проверенное), лучшим ресурсом будет модуль обратных вызовов rails.

 horseyguy23 сент. 2010 г., 20:16
он сказал, что он использовал рельсы ??!
 Swanand24 сент. 2010 г., 07:43
@ Корбин: Конечно, мы определенно можем. Но это не так, как у Rails, вот почему это предложение.
 Andrew Grimm24 сент. 2010 г., 05:32
В какой части Rails находится модуль callbacks? ActiveSupport?
 Andrew Grimm24 сент. 2010 г., 05:32
@banister: Не знаю, откуда у Суонанда такое безумное представление. Только 98% людей в рубине используют Rails.
 Swanand24 сент. 2010 г., 07:42
@banister: он никогда не делал! Я направил его в Rails, потому что он имеет похожую функциональность, которую он может изучать / копировать. @ Андрей: :-), это тоже.
 Corbin March23 сент. 2010 г., 22:49
Я считаю менее 50 строк кода в примере с Йоргом (включая урок домашнего задания). Конечно, мы можем придумать стратегию для тестирования, пока мы не посчитаем ее надежной и хорошо протестированной.
Решение Вопроса

Меньше код был изменен с оригинала. Я изменил только 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

Я только что придумал это:

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 после определяя метод, моя версия работает, только если вы ее называетедо, Тем не менее, его легко расширить, чтобы он не страдал от этой проблемы.

Также обратите внимание, что я сделал некоторые дополнительные изменения, которые не имеют отношения к проблеме, но я считаю более рубиновыми:

используйте mixin вместо класса: наследование является очень ценным ресурсом в 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
 Swanand23 сент. 2010 г., 17:42
Это просто и изящно.
 Vlad the Impala04 мар. 2013 г., 08:57
Одно замечание: alias_method загрязняет пространство имен, но использование alias_method + send приведет к более быстрому выполнению, чем получение ссылки на метод (примерно на 50% быстрее в моем тесте).

Ваш ответ на вопрос