Ruby’s inherited Hook: The Timing Problem and a TracePoint Solution in WiFi Wand
When building cross-platform Ruby libraries like WiFi Wand, 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 in WiFi Wand
Let’s say you’re building WiFi Wand’s architecture where you want to automatically verify that OS-specific model classes implement all required underscore-prefixed methods. Your first instinct might be to use the inherited hook:
class BaseModel
UNDERSCORE_PREFIXED_METHODS = %i[
_available_network_names
_connected_network_name
_disconnect
_ip_address
].freeze
def self.inherited(subclass)
puts "Verifying underscore methods for #{subclass}..."
missing_methods = UNDERSCORE_PREFIXED_METHODS - subclass.public_instance_methods
unless missing_methods.empty?
raise NotImplementedError, "Subclass #{subclass.name} must implement #{missing_methods.inspect}"
end
puts "All underscore methods implemented!"
end
end
class UbuntuModel < BaseModel
def _available_network_names
# Implementation that scans for available networks
end
def _connected_network_name
# Implementation that gets current connection
end
def _disconnect
# Implementation that disconnects from current network
end
end
Running this code produces surprising output:
Verifying underscore methods for UbuntuModel...
NotImplementedError: Subclass UbuntuModel must implement [:_available_network_names, :_connected_network_name, :_disconnect, :_ip_address]
The missing methods list includes methods that are actually defined in the subclass! But why?
Understanding the Timing Issue
The inherited hook is called immediately when Ruby encounters the class declaration line (class UbuntuModel < BaseModel), but before any of the method definitions in the class body are processed. Here’s the execution order:
- Ruby sees
class UbuntuModel < BaseModel inherited(UbuntuModel)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 a fully-defined subclass.
Let’s prove this with a more detailed example:
class BaseModel
UNDERSCORE_PREFIXED_METHODS = %i[_available_network_names _connected_network_name _disconnect].freeze
def self.inherited(subclass)
puts "inherited called for #{subclass}"
puts "Methods at this point: #{subclass.public_instance_methods(false)}"
missing = UNDERSCORE_PREFIXED_METHODS - subclass.public_instance_methods(false)
puts "Missing methods: #{missing}"
end
end
class UbuntuModel < BaseModel
puts "About to define _available_network_names method"
def _available_network_names
`nmcli -t -f SSID dev wifi list`.split("\n")
end
puts "About to define _connected_network_name method"
def _connected_network_name
`nmcli -t -f NAME,TYPE connection show --active | grep 802-11-wireless | cut -d: -f1`.strip
end
puts "About to define _disconnect method"
def _disconnect
`nmcli dev disconnect #{wifi_interface}`
end
puts "Finished defining methods"
end
puts "Final methods in UbuntuModel: #{UbuntuModel.public_instance_methods(false)}"
Output:
inherited called for UbuntuModel
Methods at this point: []
Missing methods: [:_available_network_names, :_connected_network_name, :_disconnect]
About to define _available_network_names method
About to define _connected_network_name method
About to define _disconnect method
Finished defining methods
Final methods in UbuntuModel: [:_available_network_names, :_connected_network_name, :_disconnect]
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
UNDERSCORE_PREFIXED_METHODS = %i[_available_network_names _connected_network_name _disconnect _ip_address].freeze
@pending_methods = []
def self.inherited(subclass)
subclass.instance_variable_set(:@missing_methods, UNDERSCORE_PREFIXED_METHODS.dup)
subclass.define_singleton_method(:method_added) do |method_name|
@missing_methods.delete(method_name)
puts "Added #{method_name}, still missing: #{@missing_methods}"
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 verification method:
class BaseModel
UNDERSCORE_PREFIXED_METHODS = %i[_available_network_names _connected_network_name _disconnect _ip_address].freeze
def self.verify_underscore_methods_implemented!
missing_methods = UNDERSCORE_PREFIXED_METHODS - public_instance_methods(false)
unless missing_methods.empty?
raise NotImplementedError, "#{self} must implement #{missing_methods.inspect}"
end
end
end
class UbuntuModel < BaseModel
def _available_network_names; end
def _connected_network_name; end
def _disconnect; end
verify_underscore_methods_implemented! # Manual call required
end
This works but defeats the purpose of guaranteeing that all subclasses perform verification, since it relies on the developer remembering to call the verification method in every 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
# Methods that subclasses must implement but are called via wrapper methods
UNDERSCORE_PREFIXED_METHODS = %i[
_available_network_names
_connected_network_name
_disconnect
_ip_address
].freeze
# Verify that a subclass implements all required underscore-prefixed methods
def self.verify_underscore_methods_implemented(subclass)
missing_methods = UNDERSCORE_PREFIXED_METHODS - subclass.public_instance_methods
unless missing_methods.empty?
raise NotImplementedError, "Subclass #{subclass.name} must implement #{missing_methods.inspect}"
end
end
# Automatically verify underscore methods when a subclass is inherited
def self.inherited(subclass)
# Set up a trace to detect when the subclass definition ends
trace = TracePoint.new(:end) do |tp|
# Check if we're ending the definition of our subclass
if tp.self == subclass
puts "Class #{subclass} finished loading!"
# Now we can safely inspect the fully-defined class
verify_underscore_methods_implemented(subclass)
puts "✅ All underscore methods implemented correctly!"
# Clean up - disable the trace since we only need it once
trace.disable
end
end
# Enable the trace
trace.enable
end
end
class UbuntuModel < BaseModel
def _available_network_names
# Implementation that scans for available networks
`nmcli -t -f SSID,SIGNAL dev wifi list`.split("\n").map(&:strip).reject(&:empty?)
end
def _connected_network_name
# Implementation that gets current connection
`nmcli -t -f NAME,TYPE connection show --active | grep 802-11-wireless | cut -d: -f1`.strip
end
def _disconnect
# Implementation that disconnects from current network
`nmcli dev disconnect #{wifi_interface}`
end
def _ip_address
# Implementation that gets IP address
`ip -4 addr show #{wifi_interface} | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1`.strip
end
end
class MacOsModel < BaseModel
def _available_network_names
# macOS-specific implementation using networksetup
# Implementation details...
end
def _connected_network_name
# macOS-specific implementation
# Implementation details...
end
def _disconnect
# macOS-specific implementation
# Implementation details...
end
def _ip_address
# macOS-specific implementation
# Implementation details...
end
end
Output when starting WiFi Wand:
Class UbuntuModel finished loading!
✅ All underscore methods implemented correctly!
Class MacOsModel finished loading!
✅ All underscore methods implemented correctly!
Perfect! Now we can inspect the fully-defined subclasses and verify they implement all required platform-specific methods.
How the TracePoint Solution Works in WiFi Wand
- Inherited hook fires: When
class UbuntuModel < BaseModelis encountered,inherited(UbuntuModel)is called - TracePoint setup: We create a TracePoint that listens for
:endevents (whenendkeywords are processed) - Class body processing: Ruby processes the platform-specific method implementations
- End event fires: When Ruby hits the final
endof the class definition, our TracePoint callback executes - Safe inspection: At this point, the OS-specific class is fully defined and we can safely verify its methods
- Cleanup: We disable the TracePoint since we only need it once per class
WiFi Wand’s Method Design Philosophy
In WiFi Wand, underscore-prefixed methods follow a consistent design pattern:
Internal Implementation Methods
The underscore-prefixed methods (_available_network_names, _connected_network_name, _disconnect, _ip_address) represent the core OS-specific functionality that each platform implementation must provide.
Public API Methods
The public methods that users call (available_network_names, connected_network_name, disconnect, ip_address) are provided by the BaseModel class itself. These public methods add consistent behavior like checking if wifi is on before calling the underscore-prefixed implementations.
This separation allows:
- Consistent API across all platforms regardless of OS differences
- OS-specific implementations isolated to each subclass
- Cross-cutting concerns handled in one place (state checking, error handling)
- Clean separation between what users call and what OS’s implement
TracePoint Events the actual implementation methods that each OS must provide. They’re underscore-prefixed to indicate they’re internal implementations.
2. Public Methods (wifi_on, wifi_off, detect_wifi_interface, etc.)
These are the public API methods that users call. The BaseModel provides these implementations by calling the underscore-prefixed versions with appropriate conditions (like checking if wifi is on first).
This separation allows the base class to add consistent behavior (like wifi state checking) while delegating OS-specific operations to subclasses.
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 tp.self == subclass to ensure we’re responding to the end of the specific OS-specific class we care about.
Considerations and Limitations
Performance
TracePoint has some performance overhead since it hooks into Ruby’s execution. For WiFi Wand, this isn’t a concern since:
- We only create one TracePoint per OS-specific model
- The trace is disabled immediately after use
- Model classes are loaded once at startup
Complexity
This solution is more complex than simple hooks, but the benefit is enormous: guaranteed API consistency across all OS implementations. This is crucial for a cross-platform library like WiFi Wand where each OS model must implement the same interface.
Maintenance
The approach requires good documentation. In WiFi Wand, we clearly document the UNDERSCORE_PREFIXED_METHODS array and explain that each OS model must implement these methods.
Real-World Benefits in WiFi Wand
This TracePoint pattern has been invaluable for WiFi Wand’s development:
-
Early Error Detection: Missing implementations are caught immediately when the class is defined, not at runtime when a user tries to use the functionality.
-
Consistent API: All OS models (UbuntuModel, MacOsModel, etc.) are guaranteed to implement the same interface, making WiFi Wand’s public API consistent across platforms.
-
Developer Experience: When adding a new OS model, developers get immediate feedback if they forget to implement any required methods.
-
Self-Documenting: The
UNDERSCORE_PREFIXED_METHODSarray serves as clear documentation of what each subclass needs to implement.
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 in frameworks like WiFi Wand, 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. For WiFi Wand, this means we can guarantee that every OS-specific model implements exactly the same interface, providing a consistent experience for users regardless of their operating system.
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.]