Overview
The method_undefined
hook is one of Ruby's method lifecycle callbacks that triggers when a method is completely removed from a class or module using undef_method
. Unlike method_removed
, which is called when methods are deleted with remove_method
, method_undefined
specifically responds to methods being permanently eliminated from the inheritance chain.
Ruby distinguishes between two types of method removal. The remove_method
approach removes a method definition from the current class but preserves any superclass implementations, allowing method lookup to continue up the inheritance chain. The undef_method
approach completely eliminates the method, preventing any access regardless of superclass definitions. When undef_method
executes, Ruby invokes the method_undefined
hook.
This hook mechanism enables sophisticated metaprogramming patterns where classes can respond dynamically to method removal events. The hook receives the method name as a symbol parameter, allowing classes to implement logging, validation, or compensation logic when methods are undefined.
class Example
def self.method_undefined(method_name)
puts "Method #{method_name} has been completely removed"
end
def original_method
"This method exists"
end
end
Example.undef_method(:original_method)
# => Method original_method has been completely removed
Ruby also provides singleton_method_undefined
for tracking when singleton methods are removed from specific object instances. This parallel hook system ensures comprehensive coverage of method lifecycle events across both class-level and instance-level method management.
Basic Usage
Implementing method_undefined
requires defining a class method that accepts the method name as its parameter. The hook automatically receives the symbol representing the undefined method whenever undef_method
is called.
class TrackingClass
def self.method_undefined(method_name)
@undefined_methods ||= []
@undefined_methods << method_name
puts "Undefined method: #{method_name}"
end
def self.undefined_methods
@undefined_methods || []
end
def working_method
"I work fine"
end
def deprecated_method
"This will be removed"
end
end
TrackingClass.undef_method(:deprecated_method)
# => Undefined method: deprecated_method
TrackingClass.undefined_methods
# => [:deprecated_method]
The hook executes immediately when undef_method
is called, providing real-time notification of method removal. This timing allows classes to perform cleanup operations or maintain internal state consistency.
class ResourceManager
@active_handlers = {}
def self.method_undefined(method_name)
# Clean up any resources associated with the undefined method
@active_handlers.delete(method_name)
puts "Cleaned up resources for #{method_name}"
end
def self.register_handler(method_name)
@active_handlers[method_name] = "Handler for #{method_name}"
end
def process_data
"Processing..."
end
end
ResourceManager.register_handler(:process_data)
ResourceManager.undef_method(:process_data)
# => Cleaned up resources for process_data
For singleton methods, Ruby provides the singleton_method_undefined
hook that operates on individual object instances rather than classes.
obj = Object.new
def obj.singleton_method_undefined(method_name)
puts "Singleton method #{method_name} removed from #{self}"
end
def obj.custom_behavior
"Special behavior"
end
obj.singleton_class.undef_method(:custom_behavior)
# => Singleton method custom_behavior removed from #<Object:0x...>
Advanced Usage
Complex metaprogramming scenarios often require sophisticated handling of method undefined events. Classes can implement conditional logic, method recreation, or alternative behavior installation when methods are removed.
class AdaptiveClass
@method_registry = {}
@fallback_methods = {}
def self.method_undefined(method_name)
if @method_registry[method_name]
puts "Critical method #{method_name} was undefined, reinstalling..."
define_method(method_name, &@method_registry[method_name])
elsif @fallback_methods[method_name]
puts "Installing fallback for #{method_name}"
define_method(method_name, &@fallback_methods[method_name])
else
puts "Method #{method_name} permanently removed"
end
end
def self.register_critical_method(name, &block)
@method_registry[name] = block
define_method(name, &block)
end
def self.register_fallback(name, &block)
@fallback_methods[name] = block
end
end
# Register a critical method that should be restored if removed
AdaptiveClass.register_critical_method(:essential_operation) do
"This method is essential"
end
# Register a fallback for another method
AdaptiveClass.register_fallback(:optional_feature) do
"Fallback implementation"
end
# Define the original method
AdaptiveClass.define_method(:optional_feature) do
"Original implementation"
end
# Test the behavior
AdaptiveClass.new.essential_operation
# => "This method is essential"
AdaptiveClass.undef_method(:essential_operation)
# => Critical method essential_operation was undefined, reinstalling...
AdaptiveClass.new.essential_operation
# => "This method is essential"
Hook inheritance creates additional complexity when dealing with class hierarchies. Subclasses inherit the method_undefined
behavior from their parents, but each class can also define its own hook logic.
class BaseClass
def self.method_undefined(method_name)
puts "BaseClass detected undefined method: #{method_name}"
super if defined?(super)
end
end
class DerivedClass < BaseClass
def self.method_undefined(method_name)
puts "DerivedClass handling undefined method: #{method_name}"
# Perform derived-class-specific logic
super
end
def test_method
"Testing"
end
end
DerivedClass.undef_method(:test_method)
# => DerivedClass handling undefined method: test_method
# => BaseClass detected undefined method: test_method
Module inclusion adds another layer of complexity to method_undefined
behavior. When modules define hook methods, the inclusion order affects which hooks execute and in what sequence.
module LoggingHooks
def method_undefined(method_name)
File.open('method_log.txt', 'a') do |f|
f.puts "#{Time.now}: Method #{method_name} undefined in #{self}"
end
super if defined?(super)
end
end
module ValidationHooks
def method_undefined(method_name)
if method_name.to_s.start_with?('protected_')
raise "Cannot undefine protected method: #{method_name}"
end
super if defined?(super)
end
end
class ManagedClass
extend LoggingHooks
extend ValidationHooks
def normal_method
"Normal"
end
def protected_important_method
"Important"
end
end
ManagedClass.undef_method(:normal_method)
# Logs to file and succeeds
begin
ManagedClass.undef_method(:protected_important_method)
rescue => e
puts e.message
# => Cannot undefine protected method: protected_important_method
end
Common Pitfalls
One frequent misunderstanding involves the distinction between method_removed
and method_undefined
hooks. Developers often expect method_undefined
to trigger when calling remove_method
, but Ruby only invokes this hook for undef_method
operations.
class ConfusedClass
def self.method_undefined(method_name)
puts "method_undefined called for: #{method_name}"
end
def self.method_removed(method_name)
puts "method_removed called for: #{method_name}"
end
def test_method
"Testing"
end
end
# This triggers method_removed, NOT method_undefined
ConfusedClass.remove_method(:test_method)
# => method_removed called for: test_method
ConfusedClass.define_method(:another_method) { "Another test" }
# This triggers method_undefined
ConfusedClass.undef_method(:another_method)
# => method_undefined called for: another_method
The timing of hook execution can create subtle bugs when hooks attempt to access or manipulate the undefined method. By the time method_undefined
executes, the method is already gone, making certain operations impossible.
class ProblematicClass
def self.method_undefined(method_name)
# This will fail because the method is already undefined
begin
instance = new
instance.send(method_name)
rescue NoMethodError => e
puts "Cannot call #{method_name}: #{e.message}"
end
# This will also fail
begin
method_object = instance_method(method_name)
rescue NameError => e
puts "Cannot access method object: #{e.message}"
end
end
def example_method
"Example"
end
end
ProblematicClass.undef_method(:example_method)
# => Cannot call example_method: undefined method `example_method'
# => Cannot access method object: undefined method `example_method'
Hook inheritance can produce unexpected behavior when parent and child classes both define method_undefined
but fail to call super
appropriately. This breaks the hook chain and prevents parent class logic from executing.
class ParentClass
def self.method_undefined(method_name)
puts "Parent cleanup for #{method_name}"
# Important cleanup logic here
end
end
class BrokenChild < ParentClass
def self.method_undefined(method_name)
puts "Child handling #{method_name}"
# Missing super call breaks parent cleanup
end
def test_method
"Testing"
end
end
class CorrectChild < ParentClass
def self.method_undefined(method_name)
puts "Child handling #{method_name}"
super # Properly chains to parent
end
def test_method
"Testing"
end
end
BrokenChild.undef_method(:test_method)
# => Child handling test_method
# (Parent cleanup never runs)
CorrectChild.undef_method(:test_method)
# => Child handling test_method
# => Parent cleanup for test_method
Another common mistake involves attempting to restore undefined methods within the method_undefined
hook itself. While this is technically possible, it can create infinite recursion if not handled carefully.
class RecursiveClass
@restoration_count = {}
def self.method_undefined(method_name)
@restoration_count[method_name] ||= 0
@restoration_count[method_name] += 1
if @restoration_count[method_name] > 3
puts "Stopping restoration attempts for #{method_name}"
return
end
puts "Attempting to restore #{method_name} (attempt #{@restoration_count[method_name]})"
# Be very careful with restoration logic to avoid infinite loops
define_method(method_name) do
"Restored method"
end
end
def fragile_method
"Original implementation"
end
end
# This could potentially cause issues without proper safeguards
RecursiveClass.undef_method(:fragile_method)
Reference
Hook Method Signatures
Hook Method | Scope | Parameters | Description |
---|---|---|---|
method_undefined(method_name) |
Class/Module | method_name (Symbol) |
Called when undef_method removes a method |
singleton_method_undefined(method_name) |
Object instance | method_name (Symbol) |
Called when singleton methods are undefined |
Related Methods and Operations
Method | Purpose | Triggers Hook |
---|---|---|
undef_method(symbol) |
Permanently removes method from inheritance chain | Yes - method_undefined |
remove_method(symbol) |
Removes method from current class only | No - triggers method_removed |
Module#undef_method |
Class method for undefined operations | Yes |
singleton_class.undef_method |
Undefines singleton methods | Yes - singleton_method_undefined |
Hook Execution Order
When multiple hooks are defined through inheritance or module inclusion:
- Most recently extended/included module hooks execute first
- Parent class hooks execute after child class hooks (when
super
is called) - Multiple module hooks execute in reverse inclusion order
Method Lifecycle Events
Event | Hook Triggered | Method State |
---|---|---|
Method definition | method_added |
Method available |
Method removal | method_removed |
Method unavailable, superclass accessible |
Method undefined | method_undefined |
Method permanently inaccessible |
Singleton method added | singleton_method_added |
Instance-specific method available |
Singleton method undefined | singleton_method_undefined |
Instance method permanently removed |
Best Practices
Pattern | Implementation | Use Case |
---|---|---|
Hook chaining | Always call super in hook implementations |
Preserve parent class behavior |
State tracking | Store undefined method names in class variables | Maintain removal history |
Conditional restoration | Check criteria before re-defining methods | Prevent unwanted method recreation |
Resource cleanup | Release associated resources in hooks | Memory management |
Error handling | Wrap hook logic in rescue blocks | Prevent hook failures from breaking method removal |
Common Integration Patterns
# Logging pattern
def self.method_undefined(method_name)
Rails.logger.info "Method #{method_name} undefined at #{Time.current}"
super if defined?(super)
end
# Validation pattern
def self.method_undefined(method_name)
if protected_methods.include?(method_name)
raise "Cannot undefine protected method: #{method_name}"
end
super if defined?(super)
end
# Cleanup pattern
def self.method_undefined(method_name)
@method_cache&.delete(method_name)
@handler_registry&.remove(method_name)
super if defined?(super)
end