Introduction
You wish to be informed if a class' definition changes. Keep a record of any new methods that are introduced to the class, as well as any existing methods that are changed or become undefined. It can also be helpful to be informed when a module is added to a class.
Solution
The class methods method added, method removed, and method undefined should be defined.
Ruby will pass the class's symbol into the relevant callback function whenever a method is added, removed, or left undefined.
The following sample emits a message each time a method is created, removed, or declared undefined. It throws an exception if the method essential is deleted, redefined, or left undefined:
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)
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
When a new method is added to the class, the following message is printed:
class Tracker
def new_method
'This is a new method.'
end
end
# Method "new_method" was (re)defined.
You can make a stink (more accurately, an exception) if someone modifies the required method, but you can't stop someone from doing so without freezing the class:
class Tracker
undef: important
end
# RuntimeError: The "important" method has been undefined!
We've defined the class methods (method added, method removed, and method undefined) in the Tracker class are hook methods. When specific criteria are met, some other piece of code (in this case, the Ruby interpreter) knows to call any methods with that name. Nothing extraordinary typically occurs when a method is created, withdrawn, or left undefined because the Module class specifies these methods with empty bodies.
Because of the previous code, if our Tracker class later mixes in a module, we won't be informed. We won't learn anything about the module itself or the new techniques made possible by its inclusion:
class Tracker
include Enumerable
end
# Nothing!
Detecting module inclusion is trickier. Every time a module is combined with a class in Ruby, the hook function Module#included is invoked on the module. On the other hand, we desire a hook method that is called on a specific class each time it includes a module. We must define our hook function because Ruby doesn't have one for module inclusion. To accomplish this, we must modify Module#include directly:
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
When a module is added to a class, Ruby will invoke the include hook method of that class. If we create a Tracker#include hook method, Ruby will alert us when an inclusion occurs:
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.