Перехват метода Ruby

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

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

person elasticsecurity    schedule 23.09.2010    source источник


Ответы (3)


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

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

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

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

  • используйте миксин вместо класса: наследование — очень ценный ресурс в Ruby, потому что вы можете наследовать только от одного класса. Миксины, однако, дешевы: вы можете смешивать столько, сколько хотите. Кроме того: можете ли вы действительно сказать, что Домашнее задание ЯВЛЯЕТСЯ методом перехвата?
  • используйте 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
person Jörg W Mittag    schedule 23.09.2010
comment
Одно замечание: alias_method загрязняет пространство имен, но использование alias_method + send приведет к более быстрому выполнению, чем получение ссылки на метод (примерно на 50% быстрее в моем тесте). - person Vlad the Impala; 04.03.2013

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

person Swanand    schedule 23.09.2010
comment
он сказал, что использует рельсы??! - person horseyguy; 23.09.2010
comment
Я насчитал менее 50 строк кода в примере Йорга (включая домашнее задание). Конечно, мы можем придумать стратегию для ее проверки, пока не сочтем ее надежной и хорошо протестированной. - person Corbin March; 24.09.2010
comment
@banister: Не знаю, откуда Свонанд взял это безумное представление. Только 98% пользователей ruby ​​используют Rails. - person Andrew Grimm; 24.09.2010
comment
В какой части Rails находится модуль обратных вызовов? АктивПоддержка? - person Andrew Grimm; 24.09.2010
comment
@banister: Он никогда этого не делал! Я направил его на Rails, потому что он имеет аналогичную функциональность, из которой он может учиться/копировать. @ Эндрю: :-), и это тоже. - person Swanand; 24.09.2010
comment
@Corbin: Конечно, мы определенно можем. Но это еще не так, а у Rails есть, вот почему предложение. - person Swanand; 24.09.2010