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:
- Object's singleton methods
- Object's class methods
- Included modules (reverse inclusion order)
- Parent class methods
- Parent's included modules
- Continues up inheritance chain
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 |