leemour
leemour

Reputation: 12013

Use define_method with method_added

I am trying to redefine every new method defined in a class and add functionality to it:

class Test
  def self.method_added(name)
    old_method = instance_method(name)    
    define_method(name) do |*args, &block|
      @@tests += 1
      old_method.call(*args, &block)
    end
  end
end

This results in:

stack level too deep (SystemStackError)

on line:

def self.method_added(name)

How do I do it properly?

Upvotes: 1

Views: 904

Answers (4)

estani
estani

Reputation: 26497

I needed something similar for an abstract test case class that defines setup (in minitest without hooks). I actually used your idea.

def self.method_added(method_name)
  # only override 'setup'
  return unless method_name == :setup

  # prevents recursion
  if @_rewriting
    @_rewriting = false
    return
  end
  @_rewriting = true
  setup_method = instance_method(method_name)
  define_method(method_name) do |*args, &block|
    # do something
    setup_method.bind(self).call(*args, &block)
  end
end

There where two missing things in you example:

  • The recursion anchor (stopping recursion with some flag, here @_rewriting)
  • rebinding the method (I suppose it get's unbounded because it has the same name?)

Upvotes: 0

James Rinkevich
James Rinkevich

Reputation: 121

You may want to append the method about to be defined to a list (Array) of methods so if you decide you want to memorize the methods you can clear the memoization(s) so

if !@memized_methods.include?name.to_sym then
  @memoized_methods<<name.to_sym
  define_method(name) { ...
  .
  .
  .
}
end

Upvotes: 0

leemour
leemour

Reputation: 12013

Thanks for comments, the error was caused by infinite recursion of self.method_added.
Every time define_method was called inside self.method_added, it triggered a new call to self.method_added because a new method was being defined.

To avoid this I had to set an instance variable @new_method:

class Test
  @new_method = true

  def self.method_added(name)
    if @new_method
      @new_method = false
      old_method = instance_method(name)
      define_method(name) do |*args, &block|
        @@tests += 1
        old_method.call(*args, &block)
      end
      @new_method = true
    end
  end
end

I couldn't find any better solution. Is there anything more elegant?

Upvotes: 0

Simone Carletti
Simone Carletti

Reputation: 176412

The error is caused by an infinite recursion. Once you've defined the hook, whenever you call define_method, it also triggers the method_added that in turns recalls method_added and so on.

Regardless any clean solution you may end up to, the approach itself is wrong. You said

I want to count each method call.

But using method_added will not cover several cases. In fact, method_added will not get triggered for all the methods that existed before the callback was created. Moreover, it will not work for all the methods you inherit from the parent class, including methods inherited from Object such as to_s.

If you want to count the method calls, you have other approaches.

The simplest one, without knowing too much of the Ruby internals, is to use a proxy object that delegates every method call to your object, but keeping track of the method calls. This is known as the Proxy Design Pattern.

class MethodCounter < BasicObject
  def initialize(instance)
    @counter  = 0
    @instance = instance
  end

  def method_missing(*args)
    @counter += 1
    @instance.send(*args, &block)
  end
end

Then instead of using your instance directly

t = Test.new
t.method

wrap it inside the proxy

t = MethodCounter.new(Test.new)
t.method

Another approach is to use Ruby tracking methods such as set_trace_funct to add a callback whenever a method is called.

Here's a practical example: Logging all method calls in a Rails app

Upvotes: 8

Related Questions