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