Ruby’s inherited
Hook: The Timing Problem and a TracePoint Solution
When building Ruby frameworks or libraries that need to inspect subclasses, you’ll likely encounter the inherited
hook. This powerful callback allows parent classes to respond when they’re subclassed. However, there’s a subtle timing issue that can trip up even experienced Ruby developers: the inherited
hook fires before the subclass is fully defined.
The Problem: Early Execution of inherited
Let’s say you’re building a framework where you want to automatically register all methods defined in subclasses. Your first instinct might be to use the inherited
hook:
class BaseModel
@@registered_methods = {}
def self.inherited(subclass)
puts "Registering methods for #{subclass}..."
@@registered_methods[subclass] = subclass.instance_methods(false)
puts "Found methods: #{@@registered_methods[subclass]}"
end
end
class User < BaseModel
def name
@name
end
def email
@email
end
end
Running this code produces surprising output:
Registering methods for User...
Found methods: []
The methods array is empty! But why?
Understanding the Timing Issue
The inherited
hook is called immediately when Ruby encounters the class declaration line (class User < BaseModel
), but before any of the method definitions in the class body are processed. Here’s the execution order:
- Ruby sees
class User < BaseModel
inherited(User)
is called immediately- The class body is processed (method definitions, etc.)
- Class definition completes
This timing makes inherited
perfect for setting up class-level configuration or establishing inheritance hierarchies, but useless for inspecting the fully-defined subclass.
Let’s prove this with a more detailed example:
class BaseModel
def self.inherited(subclass)
puts "inherited called for #{subclass}"
puts "Methods at this point: #{subclass.instance_methods(false)}"
end
end
class User < BaseModel
puts "About to define name method"
def name
@name
end
puts "About to define email method"
def email
@email
end
puts "Finished defining methods"
end
puts "Final methods in User: #{User.instance_methods(false)}"
Output:
inherited called for User
Methods at this point: []
About to define name method
About to define email method
Finished defining methods
Final methods in User: [:name, :email]
The inherited
hook fires before any method definitions are processed.
Alternative Approaches (And Why They Fall Short)
Method 1: method_added
Hook
You might try using method_added
to track each method as it’s defined:
class BaseModel
def self.inherited(subclass)
subclass.define_singleton_method(:method_added) do |method_name|
puts "Method #{method_name} added to #{self}"
end
end
end
This works for tracking individual methods, but it needs to run several times and doesn’t give you a clean “class is complete” callback. You’d need additional logic to determine when all methods have been defined.
Method 2: Manual Registration
You could require subclasses to manually call a registration method:
class BaseModel
def self.finalize!
puts "#{self} finalized with methods: #{instance_methods(false)}"
end
end
class User < BaseModel
def name; @name; end
def email; @email; end
finalize! # Manual call required
end
This works but defeats the purpose of guaranteeing that all subclasses perform the registration, since it relies on the developer remembering to call the registration method in the new subclass.
The Solution: TracePoint to the Rescue
Ruby’s TracePoint
API allows us to trace various execution events, including when class definitions end. We can combine this with the inherited
hook to get the best of both worlds:
class BaseModel
@@registered_methods = {}
def self.inherited(subclass)
# Set up a trace to detect when the subclass definition ends
trace = TracePoint.new(:end) do |trace_point|
# Check if we're ending the definition of our subclass
if trace_point.self == subclass
puts "Class #{subclass} finished loading!"
# Now we can safely inspect the fully-defined class
methods = subclass.instance_methods(false)
@@registered_methods[subclass] = methods
puts "Registered methods: #{methods}"
# Clean up - disable the trace since we only need it once
trace.disable
end
end
# Enable the trace
trace.enable
end
def self.registered_methods
@@registered_methods
end
end
class User < BaseModel
def name
@name
end
def email
@email
end
def age
@age
end
end
class Product < BaseModel
def title
@title
end
def price
@price
end
end
Output:
Class User finished loading!
Registered methods: [:name, :email, :age]
Class Product finished loading!
Registered methods: [:title, :price]
Perfect! Now we can inspect the fully-defined subclasses.
How the TracePoint Solution Works
- Inherited hook fires: When
class User < BaseModel
is encountered,inherited(User)
is called - TracePoint setup: We create a TracePoint that listens for
:end
events (whenend
keywords are processed) - Class body processing: Ruby processes the method definitions inside the class
- End event fires: When Ruby hits the final
end
of the class definition, our TracePoint callback executes - Safe inspection: At this point, the class is fully defined and we can safely inspect its methods
- Cleanup: We disable the TracePoint since we only need it once per class
TracePoint Events
The :end
event fires whenever Ruby processes an end
keyword, which includes:
- End of class definitions
- End of method definitions
- End of module definitions
- End of blocks
That’s why we check trace_point.self == subclass
to ensure we’re responding to the end of the specific class we care about.
Considerations and Limitations
Performance
TracePoint has some performance overhead since it hooks into Ruby’s execution. For production systems, consider:
- Only enabling traces when needed
- Disabling traces promptly after use
- Measuring impact in performance-critical applications
Complexity
This solution is more complex than simple hooks. Document it well and consider whether the automatic behavior is worth the added complexity.
Alternative Events
Depending on your needs, you might want to trace different events:
:class
- when a class is opened:call
- when methods are called:return
- when methods return
Conclusion
Ruby’s inherited
hook is powerful but has a crucial timing limitation: it fires before the subclass is fully defined. When you need to inspect or work with fully-defined subclasses, combining inherited
with TracePoint’s :end
event provides an elegant solution.
This pattern gives you the automatic behavior of inheritance hooks while ensuring you have access to the complete class definition. Just remember to balance the convenience against the added complexity and performance considerations.
The next time you’re building a Ruby framework and find yourself frustrated that inherited
doesn’t see your subclass methods, reach for TracePoint – it might be exactly what you need.
[This article was originally drafted by Claude (Anthropic) AI and subsequently edited and refined.]