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:

  1. Ruby sees class UbuntuModel < BaseModel
  2. inherited(UbuntuModel) is called immediately
  3. The class body is processed (method definitions, etc.)
  4. 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

  1. Inherited hook fires: When class UbuntuModel < BaseModel is encountered, inherited(UbuntuModel) is called
  2. TracePoint setup: We create a TracePoint that listens for :end events (when end keywords are processed)
  3. Class body processing: Ruby processes the platform-specific method implementations
  4. End event fires: When Ruby hits the final end of the class definition, our TracePoint callback executes
  5. Safe inspection: At this point, the OS-specific class is fully defined and we can safely verify its methods
  6. 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:

  1. Early Error Detection: Missing implementations are caught immediately when the class is defined, not at runtime when a user tries to use the functionality.

  2. Consistent API: All OS models (UbuntuModel, MacOsModel, etc.) are guaranteed to implement the same interface, making WiFi Wand’s public API consistent across platforms.

  3. Developer Experience: When adding a new OS model, developers get immediate feedback if they forget to implement any required methods.

  4. Self-Documenting: The UNDERSCORE_PREFIXED_METHODS array 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.]