如何“神奇”地将代码添加到ruby的所有公共类方法中?

时间:2019-06-08 16:58:07

标签: ruby metaprogramming

我希望能够在类的方法的开头和结尾插入一些代码。我也想避免重复。

我发现this answer很有帮助,但是重复并没有帮助。

class MyClass
  def initialize
    [:a, :b].each{ |method| add_code(method) }
  end

  def a
    sleep 1
    "returning from a"
  end

  def b
    sleep 1
    "returning from b"
  end

  private

  def elapsed
    start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    block_value = yield
    finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    puts "elapsed: #{finish - start} seconds, block_value: #{block_value}."
    block_value
  end

  def add_code(meth)
    meth = meth.to_sym
    self.singleton_class.send(:alias_method, "old_#{meth}".to_sym, meth)
    self.singleton_class.send(:define_method, meth) do
      elapsed do
        send("old_#{meth}".to_sym)
      end
    end
  end
end

以上方法确实有效,但是什么是更优雅的解决方案?我希望能够例如将attr_add_code放在类定义的开头,并列出要添加代码的方法,或者甚至指定将其添加到所有公共方法。 / p>

注意:self.singleton_class只是一种解决方法,因为我在初始化期间添加了代码。

4 个答案:

答案 0 :(得分:2)

如果通过重复来表示要检测的方法列表,则可以执行以下操作:

module Measure
  def self.prepended(base)
    method_names = base.instance_methods(false)

    base.instance_eval do
      method_names.each do |method_name|
        alias_method "__#{method_name}_without_timing", method_name
        define_method(method_name) do
          t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          public_send("__#{method_name}_without_timing")
          t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          puts "Method #{method_name} took #{t2 - t1}"
        end
      end
    end
  end
end

class Foo
  def a
    puts "a"
    sleep(1)
  end
  def b
    puts "b"
    sleep(2)
  end
end

Foo.prepend(Measure)

foo = Foo.new
foo.a
foo.b

# => a
# => Method a took 1.0052679998334497
# => b
# => Method b took 2.0026899999938905

主要变化是,我使用prepend,并且在prepended回调中,您可以找到在类中使用instance_methods(false)定义的方法列表,其中的false参数指示不应该考虑祖先。

答案 1 :(得分:1)

代替使用方法别名(在我看来,自从引入Module#prepend以来,它已经成为过去),我们可以在匿名模块之前添加一个匿名模块,该模块为要测量的类的每个实例方法提供一个方法。这将导致调用MyClass#a来调用此匿名模块中的方法,该方法将测量时间并仅依靠super来调用实际的MyClass#a实现。

def measure(klass)
  mod = Module.new do
    klass.instance_methods(false).each do |method|
      define_method(method) do |*args, &blk|
        start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        value = super(*args, &blk)
        finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        puts "elapsed: #{finish - start} seconds, value: #{value}."
        value
      end
    end
  end

  klass.prepend(mod)
end

或者,您可以使用class_eval,它也更快,并且允许您仅调用super而无需指定任何参数来转发方法调用中的所有参数,而{{ 1}}。

define_method

要使用此功能,只需执行以下操作:

def measure(klass)
  mod = Module.new do
    klass.instance_methods(false).each do |method|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{method}(*)
          start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          value = super
          finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          puts "elapsed: \#{finish - start} seconds, value: \#{value}."
          value
        end
      CODE
    end
  end

  klass.prepend(mod)
end

答案 2 :(得分:0)

您似乎正在尝试进行一些基准测试。您是否签出了benchmark library?在标准库中。

require 'benchmark'
puts Benchmark.measure { MyClass.new.a }
puts Benchmark.measure { MyClass.new.b }

答案 3 :(得分:0)

另一种可能性是创建一个包装类,如下所示:

class Measure < BasicObject
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args)
    t1 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
    target.public_send(name, *args)
    t2 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
    ::Kernel.puts "Method #{name} took #{t2 - t1}"
  end

  def respond_to_missing?(*args)
    target.respond_to?(*args)
  end

  private

  attr_reader :target
end

foo = Measure.new(Foo.new)

foo.a
foo.b