简体   繁体   English

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

[英]How to “magically” add code to all public class methods in ruby?

I would like to be able to insert some code at the beginning and at the end of methods in my class. 我希望能够在我的课程的方法的开头和结尾插入一些代码。 I would like to avoid repetition as well. 我也想避免重复。

I found this answer helpful, however it doesn't help with the repetition. 我发现这个答案很有帮助,但它对重复没有帮助。

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

The above does work, but what would be a more elegant solution? 以上确实有效,但更优雅的解决方案是什么? I would love to be able to, for example, put attr_add_code at the beginning of the class definition and list the methods I want the code added to, or perhaps even specify that I want it added to all public methods. 我希望能够,例如,将attr_add_code放在类定义的开头,并列出我想要添加代码的方法,或者甚至指定我希望它添加到所有公共方法中。

Note: The self.singleton_class is just a workaround since I am adding code during the initialisation. 注意: self.singleton_class只是一种解决方法,因为我在初始化期间添加代码。

If by repetition you mean the listing of methods you want to instrument, then you can do something like: 如果通过重复表示您想要检测的方法列表,那么您可以执行以下操作:

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

Main change is that i use prepend and inside the prepended callback you can find the list of methods defined on the class with instance_methods(false) , the false parameter indicating that ancestors should not be considered. 主要的变化是我使用prepend并在prepended回调中你可以找到在类上定义的方法列表,其中包含instance_methods(false)false参数表示不应该考虑祖先。

Instead of using method aliasing, which in my opinion is something of the past since the introduction of Module#prepend , we can prepend an anonymous module that has a method for each instance method of the class to be measured. 而不是使用方法别名,我认为这是自Module#prepend引入以来的过去,我们可以预先添加一个匿名模块,该模块有一个方法用于测量类的每个实例方法。 This will cause calling MyClass#a to invoke the method in this anonymous module, which measures the time and simply resorts to super to invoke the actual MyClass#a implementation. 这将导致调用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

Alternatively, you can use class_eval , which is also faster and allows you to just call super without specifying any arguments to forward all arguments from the method call, which isn't possible with define_method . 或者,您可以使用class_eval ,它也更快,并且允许您只调用super而不指定任何参数来转发方法调用中的所有参数,这对于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

To use this, simply do: 要使用它,只需:

measure(MyClass)

It looks like you're trying to do some benchmarking. 看起来你正在尝试做一些基准测试。 Have you checked out the benchmark library ? 你检查了基准库吗? It's in the standard library. 它在标准库中。

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

Another possibility would be to create a wrapper class like so: 另一种可能性是创建一个这样的包装类:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM