Reputation: 1610
I am looking for ways to ensure that a set of methods defined in my objects are all calling a particular method. To illustrate, say I have objects A and B, both have methods like so:
class A
def method_a
important_method!
end
end
class B
def method_b
important_method!
end
end
How can I easily ensure that both method_a
from A and method_b
are calling important_method!
?
In this case important_method
will come from a module that will be included in both A and B (Actually in their common superclass).
So far what I tried is to wrap both objects in a proxy that defines method_missing
and collect methods calls, but this only tells me that method_a
and method_b
are called. Any ideas?
Upvotes: 2
Views: 322
Reputation: 110685
My answer was initially to use TracePoint
, but then I read how @user2074840 used alias
, which got me thinking of using alias
in conjunctions with caller
, which leads to a very direct solution. I present that below as #2 Use Caller
.
#1 Use TracePoint
In Ruby 2.0+ you can use TracePoint to obtain the information you need. (In earlier versions, you may be able to use Kernel#set_trace_point.)
To see how this works, let's lay down some sample code:
def a
puts "a"
c
important
end
def b
puts "b"
important
end
def c
puts "c"
end
def important
puts "important"
end
We now set up for the trace, specifying the two events of interest, :call
and :return
, and the information we want to save, the event (:call
or :return
) and the method (:a
, :b
or :c
):
events = []
trace = TracePoint.trace(:call, :return) { |tp|
events << { event: tp.event, method: tp.method_id } }
Then execute the code:
4.times { send([:a, :b, :c][rand(0..2)]) }
# b
# important
# b
# important
# a
# c
# important
# c
disable trace:
trace.disable
and examine the information collected:
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}]
Note that this cannot be run in IRB
or PRY
.
We can now extract the calls to :important
as follows:
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 Use Caller
This approach uses alias
and Kernel#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"]
Here's an example of an array returned by caller
:
caller
#=> ["abc.rb:9:in `b'",
# "abc.rb:32:in `block in <main>'",
# "abc.rb:32:in `times'",
# "abc.rb:32:in `<main>'"]
It is only the first element that we use:
caller.first
#=> "abc.rb:9:in `b'",
and we apply the regex to extract the method name, which is preceded by '
'` (Ascii 96) and followed by a single quote.
Upvotes: 2
Reputation: 352
I've played a little with this. What do you think about such solution?
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...
Upvotes: 1