CrackedRuby logo

CrackedRuby

Inheritance and super

Overview

Ruby implements single inheritance where each class inherits from exactly one parent class, forming a hierarchy that terminates at BasicObject. The super keyword provides method resolution control, allowing subclasses to invoke parent implementations while extending or modifying behavior.

Every Ruby class inherits from Object by default, which inherits from BasicObject. When defining custom classes, inheritance creates an "is-a" relationship where subclasses gain access to parent methods, constants, and instance variables. The inheritance chain determines method lookup order through Ruby's method resolution algorithm.

class Animal
  def initialize(name)
    @name = name
  end
  
  def speak
    "#{@name} makes a sound"
  end
end

class Dog < Animal
  def speak
    super + " - Woof!"
  end
end

dog = Dog.new("Rex")
dog.speak
# => "Rex makes a sound - Woof!"

The super keyword comes in three forms: super with parentheses passes specific arguments, super without parentheses passes all current method arguments, and super() with empty parentheses passes no arguments. Ruby searches the inheritance hierarchy upward until finding a matching method name.

class Vehicle
  def start(fuel_type = "gasoline")
    "Starting with #{fuel_type}"
  end
end

class ElectricCar < Vehicle
  def start(battery_level = 100)
    # super passes battery_level as fuel_type
    super + " at #{battery_level}% battery"
  end
  
  def explicit_start
    # super() passes no arguments, uses default
    super() + " - Electric mode"
  end
end

Ruby's inheritance system supports method overriding, where subclasses replace parent implementations entirely, or method extension using super to build upon parent behavior. The inheritance chain remains accessible even when methods are overridden multiple levels deep.

Basic Usage

Class inheritance uses the < operator to establish parent-child relationships. Subclasses inherit all instance methods, class methods, constants, and have access to parent instance variables through inherited methods.

class Shape
  attr_reader :color
  
  def initialize(color)
    @color = color
  end
  
  def area
    raise NotImplementedError, "Subclass must implement area"
  end
  
  def description
    "A #{color} shape"
  end
end

class Rectangle < Shape
  def initialize(color, width, height)
    super(color)  # calls Shape#initialize with color
    @width = width
    @height = height
  end
  
  def area
    @width * @height
  end
  
  def description
    super + " with area #{area}"
  end
end

rect = Rectangle.new("red", 5, 3)
rect.description
# => "A red shape with area 15"

The super keyword searches the inheritance hierarchy for the next method with the same name. When called without arguments, super automatically passes all arguments from the current method. This behavior often creates the desired result but requires careful consideration of parameter compatibility.

class Logger
  def log(level, message, timestamp = Time.now)
    puts "[#{timestamp}] #{level.upcase}: #{message}"
  end
end

class FileLogger < Logger
  def initialize(filename)
    @file = File.open(filename, 'a')
  end
  
  def log(level, message, timestamp = Time.now)
    # super automatically passes level, message, timestamp
    super
    @file.puts("[#{timestamp}] #{level.upcase}: #{message}")
    @file.flush
  end
end

Method resolution follows the inheritance chain upward. Ruby searches the current class first, then the parent class, continuing until finding a matching method or reaching BasicObject. This process applies to both instance and class methods.

class A
  def self.class_method
    "A's class method"
  end
  
  def instance_method
    "A's instance method"
  end
end

class B < A
  def self.class_method
    super + " extended by B"
  end
  
  def instance_method
    super + " extended by B"
  end
end

class C < B
  def instance_method
    super + " and C"
  end
end

C.class_method
# => "A's class method extended by B"

C.new.instance_method  
# => "A's instance method extended by B and C"

Ruby supports calling super from class methods, following the same inheritance rules. Class method inheritance operates independently from instance method inheritance, creating separate resolution chains for class-level and instance-level behavior.

Advanced Usage

Multiple inheritance simulation uses modules and prepend to alter method resolution order. When modules are included, Ruby inserts them into the inheritance chain above the including class. The prepend method places modules before the class in the lookup order, affecting super behavior.

module Auditable
  def save
    puts "Auditing save operation"
    super  # calls the next save in the chain
    puts "Save operation completed"
  end
end

module Timestampable  
  def save
    @updated_at = Time.now
    super
  end
end

class Document
  prepend Auditable
  include Timestampable
  
  def save
    puts "Saving document to database"
    @saved = true
  end
end

# Method resolution order: Auditable → Document → Timestampable → Object
Document.new.save
# => Auditing save operation
# => Saving document to database  
# => Save operation completed

The method and super_method methods provide introspection capabilities for inheritance chains. These methods return Method objects that reveal the actual implementation being called and enable dynamic method invocation.

class Parent
  def compute(x)
    x * 2
  end
end

class Child < Parent
  def compute(x)
    result = super(x)
    result + 10
  end
  
  def analyze_methods
    current = method(:compute)
    parent = current.super_method
    
    puts "Current implementation: #{current.source_location}"
    puts "Parent implementation: #{parent.source_location}"
    
    # Call parent method directly
    parent_result = parent.call(5)  # => 10
    current_result = current.call(5)  # => 20
  end
end

Dynamic super invocation handles cases where method arguments change between parent and child implementations. Argument transformation often requires explicit parameter handling rather than relying on automatic argument passing.

class APIClient  
  def request(endpoint, method: :get, headers: {}, **options)
    puts "Making #{method} request to #{endpoint}"
    puts "Headers: #{headers}"
    puts "Options: #{options}"
  end
end

class AuthenticatedClient < APIClient
  def initialize(api_key)
    @api_key = api_key
  end
  
  def request(endpoint, auth: true, **options)
    if auth
      headers = options.fetch(:headers, {})
      headers['Authorization'] = "Bearer #{@api_key}"
      options[:headers] = headers
    end
    
    # Transform arguments for parent method
    super(endpoint, **options)
  end
end

client = AuthenticatedClient.new("secret123")  
client.request("/users", method: :post, body: {name: "John"})
# => Making post request to /users
# => Headers: {"Authorization"=>"Bearer secret123"}
# => Options: {:method=>:post, :body=>{:name=>"John"}}

Inheritance with singleton methods and eigenclasses creates complex method resolution scenarios. When classes inherit from parents with singleton methods, the inheritance affects both instance and class-level behavior differently.

class Database
  class << self
    def connection
      @connection ||= establish_connection
    end
    
    private
    
    def establish_connection  
      "Connected to primary database"
    end
  end
end

class ReadOnlyDatabase < Database
  class << self  
    def connection
      @connection ||= establish_readonly_connection
    end
    
    private
    
    def establish_readonly_connection
      "Connected to readonly database"
    end
    
    # Can call parent's private class method
    def establish_connection
      super + " (inherited)"
    end
  end
end

Common Pitfalls

The super keyword without parentheses passes all current method arguments, which creates problems when parent and child method signatures differ. This automatic argument passing often leads to ArgumentError exceptions when parent methods expect different parameters.

class Parent
  def process(data)
    "Processing: #{data}"
  end
end

class Child < Parent
  def process(data, options = {})
    # WRONG: super passes both data and options
    # Parent method only accepts one argument
    begin
      super  # ArgumentError: wrong number of arguments
    rescue ArgumentError => e
      puts "Error: #{e.message}"
    end
    
    # CORRECT: explicitly pass only compatible arguments
    super(data) + " with options: #{options}"
  end
end

Child.new.process("test", format: :json)

Method resolution bypasses private and protected visibility, but calling super from methods with different visibility can expose methods unintentionally. Ruby's method visibility operates independently from the inheritance chain, creating security implications.

class SecureClass
  private
  
  def sensitive_operation
    "Performing sensitive operation"
  end
  
  protected
  
  def internal_method
    sensitive_operation
  end
end

class PublicClass < SecureClass
  def public_method
    # This exposes the protected internal_method publicly
    internal_method
  end
  
  def internal_method
    # super calls protected parent method
    "Public: " + super
  end
end

obj = PublicClass.new
obj.public_method  # Unintentionally exposes protected method

Module inclusion and prepending affects super behavior in counterintuitive ways. The method resolution order changes based on how modules are mixed in, causing super calls to skip expected methods or invoke unexpected implementations.

module A
  def test
    "A: " + super
  end
end

module B  
  def test
    "B: " + super  
  end
end

class Base
  def test
    "Base"
  end
end

class IncludeOrder < Base
  include A
  include B  # B comes after A in resolution
end

class PrependOrder < Base
  prepend A  
  prepend B  # B comes before A in resolution
end

IncludeOrder.new.test
# => "B: A: Base" (B → A → Base)

PrependOrder.new.test  
# => "A: B: Base" (A → B → PrependOrder → Base)

Block passing with super requires explicit handling because blocks are not automatically forwarded. When parent methods expect blocks but child methods don't explicitly handle them, the block gets lost in the inheritance chain.

class Iterator
  def each_item(items)
    items.each { |item| yield(item) if block_given? }
  end
end

class FilteredIterator < Iterator
  def each_item(items, filter: nil)
    filtered = filter ? items.select(&filter) : items
    
    # WRONG: block is not passed to super
    super(filtered)
    
    # CORRECT: explicitly pass the block
    # super(filtered, &block)
  end
end

# This won't work as expected
FilteredIterator.new.each_item([1, 2, 3, 4]) { |n| puts n * 2 }

Infinite recursion occurs when super calls create circular dependencies in the inheritance chain. This commonly happens with module inclusion where multiple modules define the same method and call super without terminating conditions.

module Middleware1
  def process
    puts "Middleware1 before"
    super
    puts "Middleware1 after" 
  end
end

module Middleware2  
  def process
    puts "Middleware2 before"
    super  # This will cause infinite recursion if no base method exists
    puts "Middleware2 after"
  end
end

class Processor
  include Middleware1
  include Middleware2
  
  # Missing base implementation causes infinite recursion
  # def process
  #   puts "Base processing"
  # end
end

# Processor.new.process  # SystemStackError: stack level too deep

Reference

Inheritance Syntax

Syntax Description Example
class Child < Parent Defines inheritance relationship class Dog < Animal
Class.superclass Returns parent class String.superclass # => Object
obj.class.ancestors Shows inheritance chain "".class.ancestors
Class < ParentClass Tests inheritance relationship String < Object # => true

Super Keyword Forms

Form Arguments Passed Description
super All current method arguments Automatic argument forwarding
super() No arguments Explicit no arguments
super(arg1, arg2) Specified arguments only Explicit argument control
super(&block) All arguments plus explicit block Block forwarding

Method Resolution Methods

Method Returns Description
method(:name) Method object Current method implementation
method(:name).super_method Method object or nil Parent method implementation
method(:name).owner Class or Module Where method is defined
method(:name).source_location Array or nil File and line number
respond_to?(:method_name) Boolean Whether object responds to method

Class Hierarchy Methods

Method Returns Description
Class.ancestors Array of modules/classes Full inheritance chain
Class.superclass Class or nil Immediate parent class
Module.nesting Array Current nesting context
Class.included_modules Array Included modules only
obj.singleton_class Class Object's eigenclass

Visibility and Inheritance

Visibility Inherited Callable via super Description
public Yes Yes Accessible everywhere
protected Yes Yes Accessible within class hierarchy
private Yes Yes Accessible only within defining class

Module Inclusion Effects

Method Position in Chain Super Behavior
include ModuleName After class Calls continue up chain
prepend ModuleName Before class Module methods wrap class methods
extend ModuleName Singleton class Adds methods to eigenclass

Common Error Types

Error Cause Solution
ArgumentError Wrong argument count in super Use explicit arguments
NoMethodError Method not found in chain Check inheritance hierarchy
SystemStackError Infinite recursion Add base case or check module chain
NameError Undefined constant Check constant lookup rules

Inheritance Chain Examples

# Basic inheritance chain
Object.ancestors
# => [Object, Kernel, BasicObject]

# With modules  
module M; end
class C; include M; end
C.ancestors  
# => [C, M, Object, Kernel, BasicObject]

# With prepend
class D; prepend M; end  
D.ancestors
# => [M, D, Object, Kernel, BasicObject]

Method Resolution Order Rules

  1. Singleton methods (eigenclass)
  2. Modules prepended to eigenclass
  3. Eigenclass ancestors
  4. Modules prepended to class
  5. Class methods
  6. Modules included in class
  7. Superclass chain (repeat from step 4)