Ruby's method_added object lifcycle hook

What started out as a small exercise in Ruby to allow me to measure the execution times of methods has led me down several interesting paths. I've already written about my experiences with Ruby's blocks and closures here. The next thing I ran into was the method_added object life-cycle hook. Using method_added is no big deal for the experienced Rubyist, but there's a fair amount of stuff in there which isn't obvious and which took some effort on my part to understand. I also ended up with a clearer understanding of the concepts which underpin Ruby's class structure.

If you are unfamiliar with Ruby terminology, a class method behaves in a manner similar to a static method in Java or C#. I also use the words metaclass and singleton interchangeably, but either way I'm referring to the anonymous class associated with every class in Ruby.

Modifying class Class to detect the addition of methods

method_added falls into the category of introspective methods provided by Ruby itself, and is invoked whenever a method is added to a class. Here's an example - remember that in Ruby, a class is itself an object.
class Class
def one
return 1
end

def method_added(method_name)
puts "#{method_name} added to #{self}"
end

def two
return 2
end
end

class Hello
def say_it
return "Hello!"
end
end
Output:
method_added added to Class
two added to Class
say_it added to Hello

As you can see, the addition of method one isn't detected because method_added hasn't been defined. Once it has been defined, all other method additions are detected (including the addition of method_added itself!).

Modifying specific classes to detect the addition of methods

It's rarely that you'd need to detect the addition of methods to every single class in the ObjectSpace. Repeating what we did in the previous example just for a single class demonstrates some of that non-obviousness I was talking about. Let's try to detect all method additions to class Hello; the obvious solution (given below) unfortunately doesn't work.
class Hello
def method_added(method_name)
puts "#{method_name} added to #{self}"
end

def say_it
return "Hello!"
end
end

puts Hello.new.say_it
Output:
Hello!

We would expect to see say_it was added to Hello, but we don't. Let's take this step by step and figure out what's going on.

As I mentioned earlier, Hello is an instance of Class. We can create the class Hello by simply saying Hello = Class.new. Let's prove this:
def Hello.some_class_method  # => uninitialized constant Hello (NameError)
return "It worked!"
end

puts Hello.some_class_method
Now let's try again after defining Hello

Hello = Class.new

def Hello.some_class_method
return "It worked!"
end

puts Hello.some_class_method # => It worked!
So far so good. Now, we also know that adding method_added to Class allowed us to detect methods added to Hello. Obviously, Hello inherited method_added in some manner, but not as an instance method or our example above would have worked too. Let's go back to the first example and do some poking around by adding a couple of lines at the end.
class Class
def method_added(method_name)
puts "#{method_name} added to #{self}"
end
end

class Hello
def say_it
return "Hello!"
end
end

puts Hello.new.say_it
puts (Hello.methods - Hello.instance_methods).grep(/added/)
Hello.method_added("manually_invoking")
Output:
method_added added to Class
say_it added to Hello
Hello!
method_added
manually_invoking added to Hello


The first line, (Hello.methods - Hello.instance_methods).grep(/added/) first gets a collection of all methods belonging to Hello from which it removes those which are Hello's instance methods. It then searches among what's left for methods with the word 'added' in the name.

Simply put, we find that method_added has surfaced as a static or class method on Hello. The very next (and final) line in the example verifies this by actually invoking method_added and sure enough we were right - method_added is indeed a class method of Hello.

Let's test this theory now with a quick example.
class  Hello
def self.method_added(method_name)
puts "#{method_name} added to #{self}"
end

def say_it
return "Hello!"
end
end

puts Hello.new.say_it
Output:
say_it added to Hello
Hello!


So there we go, add method_added as a class method to detect the addition of methods to to that class.

The questions now are
a) Why does this work this way?
b) How do we detect the addition of class methods?

Lets tackle them in order.

It's all in the singleton class

The answer lies in the metaclass or singleton class holding Hello's meta-data (which includes its methods). Let's try to define how they relate to each other with a bit of code. I'm creating a singleton_class helper method in Object to get hold of an instance's singleton/meta class.
class Object
def singleton_class
class << self;self;end;
end
end

class Hello
end

p Hello.singleton_class.class
p Hello.singleton_class
p Hello.new.singleton_class
p Hello.singleton_class.singleton_class

puts Hello.singleton_class.superclass == Hello.new.singleton_class.superclass.superclass
Output:
Class
#<Class:Hello>
#<Class:#<Hello:0x2924904>>
#<Class:#<Class:Hello>>
true


We see that the singleton classes is of type Class. We also see that Hello and instances of Hello have their own singletons (I know that's obvious, but I thought I'd mention it anyways).

When you add a method to a class, it is added not to the class itself, but rather to its metaclass. Therefore, method_added needs to be in the metaclass, which is precisely what happens when you define a class method. Just to illustrate the point that class methods are simple methods defined on the metaclass, here are the different ways in which you can define a class method:
class Hello
def Hello.class_method_one
# ...
end
def self.class_method_two
# ...
end
class << self
def class_method_three
# ...
end
end
end
They all do the same thing - add a method to the metaclass.

Question (b), 'How do we detect the addition of class methods?', has a simpler answer: singleton_method_added. I got this answer from the internal ThoughtWorks dynamic languages list (specifically Carlos Villela and Ola Bini - thanks guys!). Use it exactly as you would method_added
class Hello
class << self
def method_added(method_name)
puts "#{method_name} added to #{self}"
end

def singleton_method_added(method_name)
puts "#{method_name} added to #{self}"
end
end
end

class Hello
def instance_method
"Hey!"
end

def self.class_method
"Dude"
end
end
Output:
singleton_method_added added to Hello
instance_method added to Hello
class_method added to Hello


To Summarise

  • method_added is an object life-cycle hook invoked whenever a method is added to a class

  • To listen for the addition of instance methods to a class, method_added must be added to that class' singleton/meta class, or, to put it another way, as a class method of the class.

  • To listen for addition of class methods, singleton_method_added must be added to the class' singleton/meta class, just like we would with method_added

1 comment:

Johnny P said...

"When you add a method to a class, it is added not to the class itself, but rather to its metaclass. Therefore, method_added needs to be in the metaclass, which is precisely what happens when you define a class method."

You are confusing instance methods with class methods here. I believe the reason method_added is in the metaclass is because an instance of the class does not exist during class definition. And during class definition the ruby is executable, your example with puts shows this. So when "def" is executed only a class method could be executed since no instance exists.