Wednesday, October 28, 2009

Recipe 10.6. Listening for Changes to a Class










Recipe 10.6. Listening for Changes to a Class




Credit: Phil Tomson



Problem


You want to be notified when the definition of a class changes. You might want to keep track of new methods added to the class, or existing methods that get removed or undefined. Being notified when a module is mixed into a class can also be useful.




Solution


Define the class methods method_added, method_removed, and/or method_undefined. Whenever the class gets a method added, removed, or undefined, Ruby will pass its symbol into the appropriate callback method.


The following example prints a message whenever a method is added, removed, or undefined. If the method "important" is removed, undefined, or redefined, it throws an exception.



class Tracker
def important
"This is an important method!"
end

def self.method_added(sym)
if sym == :important
raise 'The "important" method has been redefined!'
else
puts %{Method "#{sym}" was (re)defined.}
end
end

def self.method_removed(sym)
if sym == :important
raise 'The "important" method has been removed!'
else
puts %{Method "#{sym}" was removed.}
end
end

def self.method_undefined(sym)
if sym == :important
raise 'The "important" method has been undefined!'
else
puts %{Method "#{sym}" was removed.}
end
end
end



If someone adds a method to the class, a message will be printed:



class Tracker
def new_method
'This is a new method.'
end
end
# Method "new_method" was (re)defined.



Short of freezing the class, you can't prevent the important method from being removed, undefined, or redefined, but you can raise a stink (more precisely, an exception) if someone
changes it:



class Tracker
undef :important
end
# RuntimeError: The "important" method has been undefined!





Discussion


The class methods we've defined in the Tracker class (method_added, method_removed, and method_undefined) are hook methods. Some other piece of code (in this case, the Ruby interpreter) knows to call any methods by that name when certain conditions are met. The Module class defines these methods with empty bodies: by default, nothing special happens when a method is added, removed, or undefined.


Given the code above, we will not be notified if our tracker class later mixes in a module. We won't hear about the module itself, nor about the new methods that are available because of the module inclusion.



class Tracker
include Enumerable
end

# Nothing!



Detecting module inclusion is trickier. Ruby provides a hook method
Module#included
, which is called on a module whenever it's mixed into a class. But we want the opposite: a hook method that's called on a particular class whenever it includes a module. Since Ruby doesn't provide a hook method for module inclusion, we must define our own. To do this, we'll need to change Module#include itself.



class Module
alias_method :include_no_hook, :include
def include(*modules)
# Run the old implementation.
include_no_hook(*modules)

# Then run the hook.
modules.each do |mod|
self.include_hook mod
end
end

def include_hook
# Do nothing by default, just like Module#method_added et al.
# This method must be overridden in a subclass to do something useful.
end
end



Now when a module is included
into a class, Ruby will call that class's include_hook method. If we define a tracker#include_hook method, we can have Ruby notify us of inclusions:



class Tracker
def self.include_hook(mod)
puts %{"#{mod}" was included in #{self}.}
end
end

class Tracker
include Enumerable
end
# "Enumerable" was included in Tracker.





See Also


  • Recipe 9.3, "Mixing in Class Methods," for more on the Module#included method

  • Recipe 10.13, "Undefining a Method," for the difference between removing and undefining a method













No comments:

Post a Comment