CrackedRuby logo

CrackedRuby

Method Overriding

Overview

Method overriding in Ruby occurs when a subclass defines a method with the same name as a method in its parent class. Ruby's dynamic method resolution determines which method executes based on the object's actual class at runtime. The subclass method replaces the parent method, though the original remains accessible through the super keyword.

Ruby searches for methods in a specific order: the object's class, included modules (in reverse order of inclusion), then up the inheritance chain. This method resolution order determines which version of an overridden method executes.

class Animal
  def speak
    "Generic animal sound"
  end
end

class Dog < Animal
  def speak
    "Woof!"
  end
end

dog = Dog.new
dog.speak
# => "Woof!"

Method overriding forms the foundation of polymorphism in Ruby. Objects of different classes can respond to the same method call with class-specific behavior, enabling flexible and maintainable code design.

class Shape
  def area
    0
  end
end

class Rectangle < Shape
  def initialize(width, height)
    @width, @height = width, height
  end
  
  def area
    @width * @height
  end
end

class Circle < Shape
  def initialize(radius)
    @radius = radius
  end
  
  def area
    Math::PI * @radius * @radius
  end
end

shapes = [Rectangle.new(5, 3), Circle.new(2)]
shapes.each { |shape| puts shape.area }
# => 15
# => 12.566370614359172

The super keyword invokes the parent class method from within an overridden method. Called without arguments, super passes all current method arguments to the parent. Called with specific arguments, only those values pass to the parent method.

class Vehicle
  def initialize(make, model)
    @make = make
    @model = model
  end
  
  def description
    "#{@make} #{@model}"
  end
end

class Car < Vehicle
  def initialize(make, model, doors)
    super(make, model)
    @doors = doors
  end
  
  def description
    "#{super} with #{@doors} doors"
  end
end

car = Car.new("Toyota", "Camry", 4)
puts car.description
# => "Toyota Camry with 4 doors"

Basic Usage

Method overriding implements specialized behavior in subclasses while maintaining consistent interfaces. The subclass method completely replaces the parent method unless explicitly called through super. Ruby determines method resolution at runtime based on the object's actual class.

class Logger
  def log(message)
    puts "[#{Time.now}] #{message}"
  end
end

class FileLogger < Logger
  def initialize(filename)
    @file = File.open(filename, 'a')
  end
  
  def log(message)
    @file.puts("[#{Time.now}] #{message}")
    @file.flush
  end
  
  def close
    @file.close
  end
end

console_logger = Logger.new
file_logger = FileLogger.new('app.log')

console_logger.log("Application started")
# Outputs to console: [2023-11-15 10:30:45] Application started

file_logger.log("User logged in")
# Writes to file: [2023-11-15 10:30:46] User logged in

Abstract methods in parent classes establish interfaces that subclasses must implement. Ruby raises NotImplementedError when abstract methods execute, forcing subclass implementation.

class DataProcessor
  def process(data)
    validate(data)
    result = transform(data)
    save(result)
  end
  
  def validate(data)
    raise "Data cannot be empty" if data.nil? || data.empty?
  end
  
  def transform(data)
    raise NotImplementedError, "Subclass must implement transform method"
  end
  
  def save(result)
    raise NotImplementedError, "Subclass must implement save method"
  end
end

class CSVProcessor < DataProcessor
  def transform(data)
    data.map { |row| row.join(',') }
  end
  
  def save(result)
    File.write('output.csv', result.join("\n"))
  end
end

processor = CSVProcessor.new
processor.process([['Name', 'Age'], ['John', '30'], ['Jane', '25']])
# Creates output.csv with formatted data

Parameter handling in overridden methods requires careful consideration. The subclass method signature can differ from the parent, but method calls must remain compatible with expected usage patterns.

class Authenticator
  def authenticate(username, password)
    username == 'admin' && password == 'secret'
  end
end

class TokenAuthenticator < Authenticator
  def authenticate(token)
    # Different parameter signature
    token == 'valid_token_123'
  end
end

class DatabaseAuthenticator < Authenticator
  def authenticate(username, password, domain = 'local')
    # Extended parameter list
    super(username, password) && valid_domain?(domain)
  end
  
  private
  
  def valid_domain?(domain)
    ['local', 'corporate'].include?(domain)
  end
end

Constructor overriding with inheritance chains requires proper initialization of parent class state. The super keyword in initialize methods passes arguments to parent constructors, establishing complete object state.

class BankAccount
  def initialize(account_number, initial_balance = 0)
    @account_number = account_number
    @balance = initial_balance
  end
  
  def deposit(amount)
    @balance += amount
  end
  
  def balance
    @balance
  end
end

class SavingsAccount < BankAccount
  def initialize(account_number, initial_balance = 0, interest_rate = 0.02)
    super(account_number, initial_balance)
    @interest_rate = interest_rate
  end
  
  def add_interest
    @balance += @balance * @interest_rate
  end
end

account = SavingsAccount.new('12345', 1000, 0.05)
account.add_interest
puts account.balance
# => 1050.0

Advanced Usage

Method overriding interacts with Ruby's module system through method resolution order. When modules are included, their methods insert into the inheritance chain above the including class but below the parent class. This positioning affects overriding behavior and super calls.

module Trackable
  def save
    puts "Tracking save operation"
    log_change
    super
  end
  
  private
  
  def log_change
    puts "Change logged at #{Time.now}"
  end
end

class Model
  def save
    puts "Saving to database"
  end
end

class User < Model
  include Trackable
  
  def save
    puts "Validating user data"
    super
  end
end

user = User.new
user.save
# => Validating user data
# => Tracking save operation
# => Change logged at [timestamp]
# => Saving to database

Method aliasing preserves original method implementations when overriding. The alias keyword creates new method names for existing methods, allowing access to original behavior after overriding occurs.

class Calculator
  def add(a, b)
    a + b
  end
end

class LoggingCalculator < Calculator
  alias_method :original_add, :add
  
  def add(a, b)
    puts "Adding #{a} + #{b}"
    result = original_add(a, b)
    puts "Result: #{result}"
    result
  end
end

calc = LoggingCalculator.new
calc.add(5, 3)
# => Adding 5 + 3
# => Result: 8
# => 8

Dynamic method overriding through metaprogramming creates conditional method behavior. The define_method creates methods at runtime, enabling dynamic overriding based on configuration or runtime conditions.

class ConfigurableService
  def self.configure(options = {})
    if options[:debug]
      define_method :process do |data|
        puts "Debug: Processing #{data.inspect}"
        result = original_process(data)
        puts "Debug: Result #{result.inspect}"
        result
      end
      
      alias_method :original_process, :process unless method_defined?(:original_process)
    end
  end
  
  def process(data)
    data.upcase
  end
end

class MyService < ConfigurableService
  configure debug: true
end

service = MyService.new
service.process("hello")
# => Debug: Processing "hello"
# => Debug: Result "HELLO"
# => "HELLO"

Method overriding with blocks and yield requires careful handling of block parameters. Overridden methods must accommodate expected block usage patterns while adding specialized behavior.

class Iterator
  def each(collection)
    collection.each { |item| yield(item) }
  end
end

class FilteringIterator < Iterator
  def initialize(filter_proc)
    @filter = filter_proc
  end
  
  def each(collection)
    super(collection) do |item|
      yield(item) if @filter.call(item)
    end
  end
end

class TransformingIterator < FilteringIterator
  def initialize(filter_proc, transform_proc)
    super(filter_proc)
    @transform = transform_proc
  end
  
  def each(collection)
    super(collection) { |item| yield(@transform.call(item)) }
  end
end

numbers = [1, 2, 3, 4, 5]
even_filter = proc { |n| n.even? }
double_transform = proc { |n| n * 2 }

iterator = TransformingIterator.new(even_filter, double_transform)
iterator.each(numbers) { |n| puts n }
# => 4
# => 8

Chained method overriding across multiple inheritance levels creates complex call patterns. Each level can modify behavior while delegating to the next level through super, building layered functionality.

class BaseProcessor
  def process(data)
    puts "BaseProcessor: Starting"
    data
  end
end

class ValidationProcessor < BaseProcessor
  def process(data)
    puts "ValidationProcessor: Validating"
    raise "Invalid data" if data.nil?
    super(data)
  end
end

class TransformProcessor < ValidationProcessor
  def process(data)
    puts "TransformProcessor: Transforming"
    transformed = super(data)
    transformed.to_s.upcase
  end
end

class LoggingProcessor < TransformProcessor
  def process(data)
    puts "LoggingProcessor: Logging start"
    result = super(data)
    puts "LoggingProcessor: Logging end - #{result}"
    result
  end
end

processor = LoggingProcessor.new
result = processor.process("hello")
# => LoggingProcessor: Logging start
# => TransformProcessor: Transforming
# => ValidationProcessor: Validating
# => BaseProcessor: Starting
# => LoggingProcessor: Logging end - HELLO
# => "HELLO"

Common Pitfalls

Forgetting to call super in overridden methods breaks inheritance chains and prevents parent class functionality from executing. This commonly occurs in initialize methods where parent initialization must complete for proper object state.

class Person
  def initialize(name)
    @name = name
    @id = generate_id
  end
  
  private
  
  def generate_id
    SecureRandom.uuid
  end
end

class Employee < Person
  # WRONG: Missing super call
  def initialize(name, department)
    @department = department  # @name and @id never set
  end
  
  # CORRECT: Proper super usage
  def initialize(name, department)
    super(name)  # Ensures parent initialization
    @department = department
  end
end

# Wrong version creates incomplete objects
employee = Employee.new("John", "Engineering")
puts employee.instance_variable_get(:@name)  # => nil

Misunderstanding super argument passing creates unexpected behavior. Called without parentheses, super passes all current method arguments. Called with empty parentheses super(), no arguments pass to the parent method.

class Printer
  def print(message, prefix = "[INFO]")
    puts "#{prefix} #{message}"
  end
end

class TimestampPrinter < Printer
  def print(message, prefix = "[INFO]")
    timestamp = Time.now.strftime("%H:%M:%S")
    # WRONG: super passes both message AND prefix
    super  # Equivalent to super(message, prefix)
    # Parent receives: message="Hello", prefix="[INFO]"
  end
  
  def print_correct(message, prefix = "[INFO]")
    timestamp = Time.now.strftime("%H:%M:%S")
    # CORRECT: Explicitly control arguments
    super("#{timestamp} #{message}", prefix)
    # Parent receives modified message with timestamp
  end
end

printer = TimestampPrinter.new
printer.print("Hello")  # Unexpected output format
printer.print_correct("Hello")  # Expected timestamp prefix

Method resolution order confusion occurs when mixing modules and inheritance. Ruby searches methods in included modules before parent classes, which can override expected inheritance behavior.

class Animal
  def sound
    "Generic sound"
  end
end

module Domestic
  def sound
    "Domestic #{super}"
  end
end

class Dog < Animal
  include Domestic
  
  def sound
    "Bark!"  # This completely overrides both Animal and Domestic
  end
end

class Cat < Animal
  include Domestic
  # No sound method defined - uses module method
end

dog = Dog.new
cat = Cat.new

puts dog.sound  # => "Bark!" (not "Domestic Bark!")
puts cat.sound  # => "Domestic Generic sound"

# Method resolution order:
puts Dog.ancestors  # => [Dog, Domestic, Animal, Object, Kernel, BasicObject]
puts Cat.ancestors  # => [Cat, Domestic, Animal, Object, Kernel, BasicObject]

Return value handling in overridden methods requires attention to expected return types. Methods calling super must handle return values appropriately, especially when parent methods return different types than expected.

class DataValidator
  def validate(data)
    return false if data.nil?
    true
  end
end

class StrictValidator < DataValidator
  def validate(data)
    # WRONG: Not handling super return value
    super
    return false if data.empty?
    # Always returns nil because super return value ignored
  end
  
  def validate_correct(data)
    # CORRECT: Proper return value handling
    return false unless super(data)  # Check parent validation first
    return false if data.empty?      # Additional validation
    true                            # Explicit success return
  end
end

validator = StrictValidator.new
puts validator.validate("test")          # => nil (unexpected)
puts validator.validate_correct("test")  # => true (correct)

Infinite recursion occurs when method names conflict between parent and child classes without proper super usage. This commonly happens when overridden methods accidentally call themselves instead of the parent method.

class Counter
  def initialize
    @count = 0
  end
  
  def increment
    @count += 1
  end
end

class LoggingCounter < Counter
  def increment
    puts "Incrementing counter"
    # WRONG: This calls itself recursively
    increment  # Should be super
    # Results in infinite recursion and stack overflow
  end
  
  def increment_correct
    puts "Incrementing counter"
    super  # Properly calls parent method
  end
end

counter = LoggingCounter.new
# counter.increment  # SystemStackError: stack level too deep
counter.increment_correct  # Works correctly

Private method overriding behavior differs from public methods. Private methods in parent classes remain callable through super, but subclass private methods with the same name don't override parent public methods.

class Parent
  def public_method
    private_helper
  end
  
  private
  
  def private_helper
    "Parent private method"
  end
end

class Child < Parent
  private
  
  def private_helper
    "Child private method"
  end
end

child = Child.new
puts child.public_method  # => "Child private method"

# Private methods are inherited and can be overridden
# But visibility rules still apply

Reference

Method Override Patterns

Pattern Syntax Behavior
Complete Override def method_name; new_body; end Replaces parent method entirely
Extend with Super def method_name; super; additional_code; end Calls parent, adds behavior
Wrap with Super def method_name; before_code; super; after_code; end Surrounds parent call
Transform Arguments def method_name(args); super(modified_args); end Modifies arguments to parent
Filter Results def method_name; result = super; modify(result); end Post-processes parent result

Super Keyword Usage

Call Format Argument Passing Example
super All current method arguments def method(a, b); super; end passes a, b
super() No arguments def method(a, b); super(); end passes nothing
super(args) Specific arguments def method(a, b); super(a); end passes only a
super(*args) Array expansion def method(*args); super(*args[1..-1]); end

Method Resolution Order

Ruby searches for methods in this sequence:

  1. Object's singleton methods
  2. Object's class methods
  3. Included modules (reverse inclusion order)
  4. Parent class methods
  5. Parent's included modules
  6. Continues up inheritance chain
  7. method_missing if method not found
class Example
  def self.resolution_order
    ancestors
  end
end

# Check resolution order
puts Example.ancestors
# => [Example, Object, Kernel, BasicObject]

Common Method Override Scenarios

Scenario Implementation Pattern
Constructor Chaining def initialize(args); super(parent_args); child_setup; end
Validation Layer def method(args); validate(args); super(args); end
Logging Wrapper def method(args); log_start; result = super; log_end; result; end
Error Handling def method(args); super; rescue StandardError; handle_error; end
Performance Monitoring def method(args); time_start; result = super; measure_time; result; end
Caching Layer def method(args); return cache[args] if cached?; cache[args] = super; end

Inheritance and Module Interactions

Combination Method Resolution Super Behavior
Class inherits, no modules Direct parent lookup Calls parent class method
Class includes module Module method first Module's super calls parent class
Module includes module Nested module chain Follows module inclusion order
Multiple module inclusion Last included first Reverse inclusion order traversal

Error Conditions

Error Type Cause Example
NoMethodError Method not found in chain Calling undefined parent method
SystemStackError Infinite recursion Method calls itself instead of super
ArgumentError Wrong argument count Super called with incompatible arguments
NotImplementedError Abstract method called Parent method raises explicitly

Metaprogramming Override Techniques

Technique Method Usage
Alias Original alias_method :original_method, :method Preserve original implementation
Define Dynamic define_method(:name) { block } Runtime method creation
Remove Method remove_method :method_name Force inheritance lookup
Undefine Method undef_method :method_name Prevent method calls entirely

Testing Override Behavior

# Verify method resolution
object.method(:method_name).owner  # Shows defining class
object.method(:method_name).source_location  # Shows file and line

# Check method ancestry
ClassName.ancestors  # Shows inheritance chain
ClassName.included_modules  # Shows included modules

# Verify super calls
# Use method tracing or debugging tools

Performance Considerations

Factor Impact Optimization
Deep Inheritance Slower method lookup Limit inheritance depth
Multiple Modules Increased resolution time Minimize module inclusion
Dynamic Methods Runtime overhead Define at class level when possible
Super Calls Additional lookup cost Cache results when appropriate