Рубиновый метод перехвата - PullRequest
5 голосов
/ 23 сентября 2010

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

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

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

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

  • используйте миксин вместо класса: наследование является очень ценным ресурсом в 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
2 голосов
/ 23 сентября 2010

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

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

...