Ruby - 捕获从方法体发送的方法调用

Ruby - Capture method calls sent from method body

我正在寻找确保我的对象中定义的一组方法都调用特定方法的方法。为了说明,假设我有对象 A 和 B,它们都有这样的方法:

class A
  def method_a
     important_method!
  end
end

class B
  def method_b
     important_method!
  end
end

如何轻松确保来自 A 的 method_amethod_b 都在呼叫 important_method!

在这种情况下,important_method 将来自将包含在 A 和 B 中的模块(实际上是在它们的公共超类中)。

到目前为止,我尝试的是将两个对象包装在定义 method_missing 和收集方法调用的代理中,但这只告诉我调用了 method_amethod_b。有任何想法吗?

我的答案最初是使用 TracePoint,但后来我读到 @user2074840 如何使用 alias,这让我想到将 aliascaller 结合使用, 这导致了一个非常直接的解决方案。我在下面将其表示为 #2 Use Caller.

#1 使用 TracePoint

在 Ruby 2.0+ 中你可以使用 TracePoint to obtain the information you need. (In earlier versions, you may be able to use Kernel#set_trace_point.)

为了了解这是如何工作的,让我们写一些示例代码:

def a
  puts "a"
  c
  important
end

def b
  puts "b"
  important
end

def c
  puts "c"
end

def important
  puts "important"
end

我们现在设置跟踪,指定两个感兴趣的事件,:call:return,以及我们要保存的信息,事件(:call:return) 和方法 (:a, :b:c):

events = []
trace = TracePoint.trace(:call, :return) { |tp|
  events <<  { event: tp.event, method: tp.method_id } }

然后执行代码:

4.times { send([:a, :b, :c][rand(0..2)]) }
  # b
  # important
  # b
  # important
  # a
  # c
  # important
  # c

禁用跟踪:

trace.disable

并检查收集到的信息:

p events
  # [{:event=>:call,   :method=>:b},
  #  {:event=>:call,   :method=>:important},
  #  {:event=>:return, :method=>:important},
  #  {:event=>:return, :method=>:b},
  #  {:event=>:call,   :method=>:b},
  #  {:event=>:call,   :method=>:important},
  #  {:event=>:return, :method=>:important},
  #  {:event=>:return, :method=>:b},
  #  {:event=>:call,   :method=>:a},
  #  {:event=>:call,   :method=>:c},
  #  {:event=>:return, :method=>:c},
  #  {:event=>:call,   :method=>:important},
  #  {:event=>:return, :method=>:important},
  #  {:event=>:return, :method=>:a},
  #  {:event=>:call,   :method=>:c},
  #  {:event=>:return, :method=>:c}]

请注意,这不能是 IRBPRY 中的 运行。

我们现在可以提取对 :important 的调用,如下所示:

def calls_to_method(events, method) 
  stack = []
  events.each_with_object([]) do |h, calling_methods|
    stack << h
    while stack.size > 1 &&
          stack[-1][:event] == :return &&
          stack[-2][:event] == :call &&
          stack[-1][:method] == stack[-2][:method] do
      if (stack.size > 2 && (stack[-1][:method] == method))
        calling_methods << stack[-3][:method]
      end
      stack.pop
      stack.pop
    end  
  end
end

calls = calls_to_method(events, :important)
  #=> [:b, :b, :a]

calls.uniq
  #=> [:b, :a]

#2 使用来电

此方法使用 aliasKernel#caller:

@calling_methods = []
alias :old_important :important

def important
  @calling_methods << caller.first[/`(.*)'/,1]
  old_important
end

4.times { send([:a, :b, :c][rand(0..2)]) }
  # b
  # important
  # c
  # a
  # c
  # important
  # a
  # c
  # important
  # b
  # a
  # a

p @calling_methods
  #=> ["b", "a", "a"]

这是 caller 返回的数组示例:

caller
  #=> ["abc.rb:9:in `b'",
  #    "abc.rb:32:in `block in <main>'",
  #    "abc.rb:32:in `times'",
  #    "abc.rb:32:in `<main>'"]

我们使用的只是第一个元素:

caller.first
  #=> "abc.rb:9:in `b'",

我们应用正则表达式提取方法名称,方法名称前面是 ''` (Ascii 96),后面是单引号。

我玩过这个。您如何看待这样的解决方案?

module CallImportantMethod

  def important_method!
    puts 'Important Method called!'
  end 

  def self.included(base)


    base.instance_methods(false).each do |method_name|

      base.class_eval do
        alias_method :"old_#{method_name}", method_name 
      end

      base.class_eval <<-eoruby
        def #{method_name}(*args, &block)
          important_method!
          old_#{method_name}(*args, &block)
        end
      eoruby

    end 
  end

end 

class SomeClass

  def testing 
    puts 'My Method was called'
  end 

  def testing_with_args(a,b)
    puts a + b
  end 

  def testing_with_block
    yield
  end 

  include CallImportantMethod

end 

i = SomeClass.new 
i.testing 
# => Important Method called!
# => My Method was called
i.testing_with_args(5,8)
# => Important Method called!
# => 13
i.testing_with_block { puts 'passing block...' } 
# => Important Method called!
# => passing block...