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 methodsmethod_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 withmethod_added
1 comment:
"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.
Post a Comment