Как «волшебным образом» добавить код во все методы публичного класса в ruby? - PullRequest
1 голос
/ 08 июня 2019

Я хотел бы иметь возможность вставить некоторый код в начале и в конце методов в моем классе. Я также хотел бы избежать повторения.

Я нашел этот ответ полезным, однако он не помогает с повторением.

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

Примечание: self.singleton_class - это просто обходной путь, так как я добавляю код во время инициализации.

Ответы [ 4 ]

2 голосов
/ 08 июня 2019

Если под повторением вы имеете в виду список методов, которые хотите использовать, то вы можете сделать что-то вроде:

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 голос
/ 09 июня 2019

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

measure(MyClass)
0 голосов
/ 09 июня 2019

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

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
0 голосов
/ 08 июня 2019

Похоже, вы пытаетесь сделать какой-то тест.Вы проверили библиотеку тестов ?Это в стандартной библиотеке.

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