Overview
Method overloading and overriding represent two distinct mechanisms for achieving polymorphism in object-oriented programming. Method overloading allows multiple methods to share the same name while accepting different parameter signatures, enabling the same operation to behave differently based on input types. Method overriding occurs when a subclass provides a specific implementation for a method already defined in its parent class, allowing child classes to customize inherited behavior.
These concepts originated in early object-oriented languages like Simula and Smalltalk, becoming fundamental features in languages like C++, Java, and C#. The distinction between compile-time polymorphism (overloading) and runtime polymorphism (overriding) shapes how languages handle method resolution and type checking.
Ruby's approach differs significantly from traditional statically-typed languages. Ruby does not support method overloading in the conventional sense because methods are identified solely by name, not by parameter signatures. When multiple methods with the same name are defined, the last definition replaces all previous ones. Ruby achieves overloading-like behavior through optional parameters, variable-length argument lists, keyword arguments, and duck typing.
# Last definition wins - no traditional overloading
class Calculator
def add(a, b)
a + b
end
def add(a, b, c) # This replaces the previous add method
a + b + c
end
end
calc = Calculator.new
calc.add(1, 2) # ArgumentError: wrong number of arguments (given 2, expected 3)
calc.add(1, 2, 3) # => 6
Method overriding functions as expected in Ruby through inheritance. A subclass can define a method with the same name as its parent, and Ruby's method lookup mechanism ensures the child's implementation executes. The super keyword provides access to the parent implementation, enabling controlled extension of inherited behavior.
Key Principles
Method Overloading refers to defining multiple methods with identical names but different parameter lists within the same class or scope. The compiler or interpreter selects which method to invoke based on the number, types, and order of arguments passed during the call. Languages supporting overloading perform this resolution at compile time (static dispatch) or runtime (dynamic dispatch) depending on their type systems.
Traditional overloading relies on method signatures that include the method name plus parameter type information. Two methods are considered distinct if they differ in parameter count, parameter types, or parameter order. Return types alone cannot distinguish overloaded methods because the compiler cannot determine which version to call based solely on how the return value will be used.
# Ruby simulates overloading using optional parameters
class DataProcessor
def process(data, format: :json, validate: true)
validated_data = validate ? validate_data(data) : data
case format
when :json
convert_to_json(validated_data)
when :xml
convert_to_xml(validated_data)
when :csv
convert_to_csv(validated_data)
end
end
private
def validate_data(data)
# Validation logic
data
end
def convert_to_json(data)
require 'json'
JSON.generate(data)
end
def convert_to_xml(data)
# XML conversion
end
def convert_to_csv(data)
# CSV conversion
end
end
processor = DataProcessor.new
processor.process({name: "test"}) # Uses defaults
processor.process({name: "test"}, format: :xml) # Overrides format
processor.process({name: "test"}, format: :csv, validate: false) # Multiple overrides
Method Overriding occurs within inheritance hierarchies when a subclass provides its own implementation of a method defined in a parent class. The subclass method must have the same name and compatible parameter signature as the parent method. When an overridden method is called on a subclass instance, the subclass version executes through dynamic dispatch, where the runtime determines which implementation to invoke based on the object's actual type.
Dynamic dispatch operates through virtual method tables (vtables) in compiled languages or method lookup algorithms in interpreted languages. The system checks the object's class for the method first, then walks up the inheritance chain if not found. This mechanism enables the Liskov Substitution Principle, where subclass instances can replace parent class instances without affecting program correctness.
# Method overriding with inheritance
class Shape
def area
raise NotImplementedError, "Subclasses must implement area"
end
def perimeter
raise NotImplementedError, "Subclasses must implement perimeter"
end
def describe
"This is a shape with area #{area} and perimeter #{perimeter}"
end
end
class Rectangle < Shape
attr_reader :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
width * height
end
def perimeter
2 * (width + height)
end
end
class Circle < Shape
attr_reader :radius
def initialize(radius)
@radius = radius
end
def area
Math::PI * radius ** 2
end
def perimeter
2 * Math::PI * radius
end
end
shapes = [Rectangle.new(5, 3), Circle.new(4)]
shapes.each { |shape| puts shape.describe }
# Output uses overridden area and perimeter methods
The super keyword accesses the parent class implementation from within an overriding method. Without arguments, super passes all current method arguments to the parent. With an empty argument list super(), it passes no arguments. With specific arguments super(arg1, arg2), it passes exactly those values. This enables extending parent behavior rather than completely replacing it.
Ruby's method lookup follows a specific path: singleton class, class, included modules (in reverse order of inclusion), superclass, superclass modules, and continuing up the chain. The first method found with matching name executes. This path explains how overriding works and why module methods can be overridden by class methods.
Ruby Implementation
Ruby's single-dispatch dynamic type system identifies methods by name alone, making traditional overloading impossible. Each method name maps to exactly one implementation within a class. Defining a method with an existing name replaces the previous definition completely, unlike languages that maintain multiple method bodies distinguished by signatures.
Ruby provides several mechanisms to achieve overloading-like flexibility. Optional parameters with default values allow a single method to handle variable argument counts. The splat operator captures remaining arguments into an array, supporting arbitrary argument counts. Keyword arguments enable named parameters with defaults, and double-splat captures extra keyword arguments into a hash.
# Optional parameters for flexible method calls
class StringFormatter
def format(text, uppercase: false, truncate: nil, prefix: '', suffix: '')
result = text.dup
result = prefix + result + suffix unless prefix.empty? && suffix.empty?
result = result.upcase if uppercase
result = result[0...truncate] + '...' if truncate && result.length > truncate
result
end
end
formatter = StringFormatter.new
formatter.format("hello world")
# => "hello world"
formatter.format("hello world", uppercase: true)
# => "HELLO WORLD"
formatter.format("hello world", truncate: 8, suffix: '!')
# => "hello wo...!"
formatter.format("hello world", prefix: '>>> ', uppercase: true, truncate: 15)
# => ">>> HELLO WORLD"
Variable-length arguments through the splat operator provide overloading-like behavior for methods that work with arbitrary collections. The splat converts multiple arguments into an array, and double-splat handles keyword arguments as a hash. This pattern appears frequently in Ruby core methods and standard library.
# Splat operators for variable arguments
class MathOperations
def sum(*numbers)
numbers.reduce(0, :+)
end
def product(*numbers)
numbers.reduce(1, :*)
end
def configure(**options)
defaults = { precision: 2, rounding: :half_up, notation: :standard }
config = defaults.merge(options)
@precision = config[:precision]
@rounding = config[:rounding]
@notation = config[:notation]
config
end
def calculate(operation, *operands, **options)
configure(**options)
case operation
when :sum
sum(*operands)
when :product
product(*operands)
else
raise ArgumentError, "Unknown operation: #{operation}"
end
end
end
math = MathOperations.new
math.sum(1, 2, 3, 4, 5) # => 15
math.product(2, 3, 4) # => 24
math.configure(precision: 4, notation: :scientific)
math.calculate(:sum, 1, 2, 3, precision: 3, rounding: :floor)
Duck typing complements Ruby's approach by focusing on object behavior rather than type. Methods check for capabilities through respond_to? or attempt operations and handle exceptions, allowing polymorphic behavior without inheritance. This paradigm replaces some overloading use cases by accepting any object supporting the required interface.
# Duck typing as alternative to overloading
class DataSerializer
def serialize(data)
if data.respond_to?(:to_json)
data.to_json
elsif data.respond_to?(:to_h)
require 'json'
JSON.generate(data.to_h)
elsif data.respond_to?(:to_a)
require 'json'
JSON.generate(data.to_a)
else
data.to_s
end
end
end
class CustomObject
def initialize(name, value)
@name = name
@value = value
end
def to_h
{ name: @name, value: @value }
end
end
serializer = DataSerializer.new
serializer.serialize([1, 2, 3])
# => "[1,2,3]"
serializer.serialize(CustomObject.new("test", 42))
# => "{\"name\":\"test\",\"value\":42}"
serializer.serialize("plain string")
# => "plain string"
Method overriding works through Ruby's inheritance mechanism. When a class inherits from a parent, it gains access to all parent methods. Defining a method with the same name in the child class overrides the parent implementation. Ruby's method lookup checks the object's class first, then walks the ancestor chain, ensuring child methods take precedence.
# Method overriding with super
class Logger
attr_reader :log_level
def initialize(log_level = :info)
@log_level = log_level
@logs = []
end
def log(message, level: :info)
return unless should_log?(level)
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
formatted = format_message(timestamp, level, message)
@logs << formatted
output(formatted)
end
protected
def should_log?(level)
level_priority(level) >= level_priority(@log_level)
end
def format_message(timestamp, level, message)
"[#{timestamp}] #{level.to_s.upcase}: #{message}"
end
def output(message)
puts message
end
def level_priority(level)
{ debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }[level] || 1
end
end
class FileLogger < Logger
def initialize(filename, log_level = :info)
super(log_level)
@filename = filename
@file = File.open(filename, 'a')
end
def log(message, level: :info)
super # Calls parent log method with all arguments
flush
end
protected
def output(message)
@file.puts(message) # Override to write to file instead of stdout
end
def flush
@file.flush
end
def close
@file.close
end
end
# FileLogger overrides output but extends log
file_logger = FileLogger.new('app.log', :debug)
file_logger.log("Application started", level: :info)
file_logger.log("Debug information", level: :debug)
Modules provide another form of overriding through mixins. When a module is included, its methods become available to the class. If the class defines methods with the same names, the class methods override the module methods. If multiple modules define the same method, the last included module's method wins, following Ruby's method resolution order.
# Module mixins and method precedence
module Validation
def validate
"Module validation"
end
def process
validate_data
perform_processing
end
protected
def validate_data
puts "Validating from module"
end
def perform_processing
puts "Processing from module"
end
end
class DataHandler
include Validation
def validate
"Class validation overrides module"
end
protected
def validate_data
puts "Validating from class"
super # Calls module's validate_data - but there isn't one!
rescue NoMethodError
puts "No parent validate_data"
end
end
handler = DataHandler.new
puts handler.validate
# => "Class validation overrides module"
handler.process
# Output:
# Validating from class
# No parent validate_data
# Processing from module
Practical Examples
Method overloading simulation through optional and keyword arguments appears throughout Ruby applications. HTTP client libraries demonstrate this pattern, providing flexible method signatures for different request configurations while maintaining a clean interface.
# HTTP client with flexible method signatures
class HttpClient
def initialize(base_url, default_headers: {}, timeout: 30)
@base_url = base_url
@default_headers = default_headers
@timeout = timeout
end
def get(path, params: {}, headers: {}, timeout: nil)
request(:get, path, params: params, headers: headers, timeout: timeout)
end
def post(path, body: nil, params: {}, headers: {}, timeout: nil)
request(:post, path, body: body, params: params, headers: headers, timeout: timeout)
end
def request(method, path, body: nil, params: {}, headers: {}, timeout: nil)
url = build_url(path, params)
merged_headers = @default_headers.merge(headers)
request_timeout = timeout || @timeout
# Actual HTTP request would go here
{
method: method,
url: url,
headers: merged_headers,
body: body,
timeout: request_timeout
}
end
private
def build_url(path, params)
url = @base_url + path
return url if params.empty?
query_string = params.map { |k, v| "#{k}=#{v}" }.join('&')
"#{url}?#{query_string}"
end
end
# Different call patterns using same methods
client = HttpClient.new('https://api.example.com', default_headers: { 'User-Agent' => 'RubyApp' })
# Simple GET request
client.get('/users')
# GET with query parameters
client.get('/users', params: { page: 2, limit: 50 })
# GET with custom headers and timeout
client.get('/users', params: { id: 123 }, headers: { 'Authorization' => 'Bearer token' }, timeout: 60)
# POST with body
client.post('/users', body: { name: 'John', email: 'john@example.com' })
# POST with all options
client.post('/users',
body: { name: 'Jane' },
params: { notify: true },
headers: { 'Content-Type' => 'application/json' },
timeout: 45
)
Method overriding enables template method patterns where parent classes define algorithm structure and child classes customize specific steps. Database adapter classes demonstrate this pattern, sharing connection logic while customizing query execution for different database systems.
# Database adapter with template method pattern
class DatabaseAdapter
def initialize(config)
@config = config
@connection = nil
end
def execute_query(sql, params = [])
connect unless connected?
prepared = prepare_statement(sql)
bound = bind_parameters(prepared, params)
result = execute_statement(bound)
format_results(result)
ensure
cleanup_resources
end
def connect
@connection = establish_connection
configure_connection(@connection)
end
def disconnect
return unless connected?
close_connection(@connection)
@connection = nil
end
def connected?
!@connection.nil? && connection_alive?(@connection)
end
protected
# Template methods to be overridden
def establish_connection
raise NotImplementedError, "Subclasses must implement establish_connection"
end
def prepare_statement(sql)
sql # Default: no preparation
end
def bind_parameters(statement, params)
statement # Default: no binding
end
def execute_statement(statement)
raise NotImplementedError, "Subclasses must implement execute_statement"
end
def format_results(result)
result # Default: no formatting
end
def configure_connection(connection)
# Default: no additional configuration
end
def connection_alive?(connection)
true # Default: assume alive
end
def close_connection(connection)
# Default: no-op
end
def cleanup_resources
# Default: no cleanup
end
end
class PostgresAdapter < DatabaseAdapter
protected
def establish_connection
require 'pg'
PG::Connection.new(
host: @config[:host],
port: @config[:port],
dbname: @config[:database],
user: @config[:username],
password: @config[:password]
)
end
def prepare_statement(sql)
# PostgreSQL uses $1, $2 for parameters
counter = 0
sql.gsub('?') { counter += 1; "$#{counter}" }
end
def execute_statement(statement)
@connection.exec(statement)
end
def format_results(result)
result.map { |row| row.transform_keys(&:to_sym) }
end
def configure_connection(connection)
connection.exec("SET client_encoding TO 'UTF8'")
connection.exec("SET timezone TO 'UTC'")
end
def connection_alive?(connection)
connection.status == PG::CONNECTION_OK
end
def close_connection(connection)
connection.close
end
end
class MySQLAdapter < DatabaseAdapter
protected
def establish_connection
require 'mysql2'
Mysql2::Client.new(
host: @config[:host],
port: @config[:port],
database: @config[:database],
username: @config[:username],
password: @config[:password]
)
end
def execute_statement(statement)
@connection.query(statement)
end
def format_results(result)
result.to_a
end
def configure_connection(connection)
connection.query("SET NAMES 'utf8mb4'")
connection.query("SET time_zone = '+00:00'")
end
def connection_alive?(connection)
connection.ping
end
def close_connection(connection)
connection.close
end
end
# Both adapters use the same interface with customized behavior
postgres = PostgresAdapter.new(host: 'localhost', port: 5432, database: 'myapp')
postgres.execute_query("SELECT * FROM users WHERE id = ?", [123])
mysql = MySQLAdapter.new(host: 'localhost', port: 3306, database: 'myapp')
mysql.execute_query("SELECT * FROM users WHERE id = ?", [123])
Strategy pattern implementations combine overriding with composition, allowing runtime algorithm selection. Payment processing systems exemplify this, where different payment methods override core processing logic while sharing validation and logging infrastructure.
# Payment processor with strategy pattern
class PaymentProcessor
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
@amount = amount
@currency = currency
@status = :pending
end
def process(payment_method)
validate_payment
begin
result = payment_method.charge(amount, currency)
@status = result[:success] ? :completed : :failed
log_transaction(payment_method, result)
result
rescue => e
@status = :error
log_error(payment_method, e)
{ success: false, error: e.message }
end
end
private
def validate_payment
raise ArgumentError, "Amount must be positive" unless amount > 0
raise ArgumentError, "Invalid currency" unless valid_currency?(currency)
end
def valid_currency?(curr)
%w[USD EUR GBP JPY].include?(curr)
end
def log_transaction(method, result)
puts "[#{Time.now}] #{method.class.name}: #{amount} #{currency} - #{result[:success] ? 'SUCCESS' : 'FAILED'}"
end
def log_error(method, error)
puts "[#{Time.now}] #{method.class.name}: ERROR - #{error.message}"
end
end
# Base payment method
class PaymentMethod
def charge(amount, currency)
raise NotImplementedError, "Subclasses must implement charge"
end
protected
def format_amount(amount, currency)
"#{currency} #{sprintf('%.2f', amount)}"
end
end
class CreditCardPayment < PaymentMethod
def initialize(card_number, expiry, cvv)
@card_number = card_number
@expiry = expiry
@cvv = cvv
end
def charge(amount, currency)
# Simulate credit card validation
return { success: false, error: 'Invalid card' } unless valid_card?
# Simulate payment gateway call
transaction_id = SecureRandom.uuid
{
success: true,
transaction_id: transaction_id,
method: 'credit_card',
amount: format_amount(amount, currency)
}
end
private
def valid_card?
@card_number.length >= 13 && @cvv.length >= 3
end
end
class PayPalPayment < PaymentMethod
def initialize(email, password)
@email = email
@password = password
@authenticated = false
end
def charge(amount, currency)
authenticate unless @authenticated
return { success: false, error: 'Authentication failed' } unless @authenticated
# Simulate PayPal API call
{
success: true,
transaction_id: "PP-#{SecureRandom.hex(8)}",
method: 'paypal',
amount: format_amount(amount, currency),
email: @email
}
end
private
def authenticate
# Simulate authentication
@authenticated = @email.include?('@') && @password.length >= 8
end
end
class BitcoinPayment < PaymentMethod
def initialize(wallet_address)
@wallet_address = wallet_address
end
def charge(amount, currency)
btc_amount = convert_to_bitcoin(amount, currency)
# Simulate blockchain transaction
{
success: true,
transaction_id: "BTC-#{SecureRandom.hex(16)}",
method: 'bitcoin',
amount: format_amount(amount, currency),
btc_amount: btc_amount,
wallet: @wallet_address
}
end
private
def convert_to_bitcoin(amount, currency)
# Simplified conversion
rates = { 'USD' => 50000, 'EUR' => 45000, 'GBP' => 40000 }
(amount / rates[currency]).round(8)
end
end
# Using different payment methods with same processor
processor = PaymentProcessor.new(100.00, 'USD')
credit_card = CreditCardPayment.new('4111111111111111', '12/25', '123')
result1 = processor.process(credit_card)
paypal = PayPalPayment.new('user@example.com', 'securepass123')
result2 = processor.process(paypal)
bitcoin = BitcoinPayment.new('1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa')
result3 = processor.process(bitcoin)
Common Patterns
The parameter object pattern addresses method overloading limitations by encapsulating multiple parameters into a single object. This pattern reduces method signature complexity, makes parameter addition non-breaking, and provides clear parameter documentation through object attributes.
# Parameter object pattern
class SearchOptions
attr_accessor :query, :filters, :sort_by, :sort_order, :page, :per_page,
:include_archived, :fuzzy_match
def initialize(query: nil, filters: {}, sort_by: :relevance, sort_order: :desc,
page: 1, per_page: 20, include_archived: false, fuzzy_match: false)
@query = query
@filters = filters
@sort_by = sort_by
@sort_order = sort_order
@page = page
@per_page = per_page
@include_archived = include_archived
@fuzzy_match = fuzzy_match
end
def offset
(page - 1) * per_page
end
def limit
per_page
end
end
class SearchService
def search(options)
options = SearchOptions.new(**options) if options.is_a?(Hash)
results = build_query(options)
results = apply_filters(results, options.filters)
results = exclude_archived(results) unless options.include_archived
results = apply_sorting(results, options.sort_by, options.sort_order)
results = paginate(results, options.offset, options.limit)
results
end
private
def build_query(options)
# Query building logic
[]
end
def apply_filters(results, filters)
results # Filtering logic
end
def exclude_archived(results)
results.reject { |r| r[:archived] }
end
def apply_sorting(results, field, order)
results.sort_by { |r| r[field] }
end
def paginate(results, offset, limit)
results[offset, limit] || []
end
end
# Clean method calls with parameter object
search = SearchService.new
# Simple search
search.search(query: "Ruby programming")
# Complex search with multiple options
search.search(
query: "Ruby programming",
filters: { category: 'tutorial', difficulty: 'advanced' },
sort_by: :date,
sort_order: :desc,
page: 2,
per_page: 50,
include_archived: true,
fuzzy_match: true
)
# Can also pass SearchOptions directly
options = SearchOptions.new(query: "Rails", page: 3)
search.search(options)
The builder pattern provides an alternative to complex constructors and initialization methods, allowing fluent, step-by-step object configuration. This pattern works particularly well for objects with many optional attributes.
# Builder pattern for complex object construction
class QueryBuilder
def initialize
@select_columns = ['*']
@from_table = nil
@joins = []
@where_conditions = []
@group_by_columns = []
@having_conditions = []
@order_by_clauses = []
@limit_value = nil
@offset_value = nil
end
def select(*columns)
@select_columns = columns
self
end
def from(table)
@from_table = table
self
end
def join(table, on:, type: :inner)
@joins << { table: table, on: on, type: type }
self
end
def where(condition, *params)
@where_conditions << { condition: condition, params: params }
self
end
def group_by(*columns)
@group_by_columns = columns
self
end
def having(condition, *params)
@having_conditions << { condition: condition, params: params }
self
end
def order_by(column, direction: :asc)
@order_by_clauses << { column: column, direction: direction }
self
end
def limit(value)
@limit_value = value
self
end
def offset(value)
@offset_value = value
self
end
def build
sql = "SELECT #{@select_columns.join(', ')}"
sql += " FROM #{@from_table}"
@joins.each do |join|
sql += " #{join[:type].to_s.upcase} JOIN #{join[:table]} ON #{join[:on]}"
end
unless @where_conditions.empty?
conditions = @where_conditions.map { |w| w[:condition] }.join(' AND ')
sql += " WHERE #{conditions}"
end
unless @group_by_columns.empty?
sql += " GROUP BY #{@group_by_columns.join(', ')}"
end
unless @having_conditions.empty?
conditions = @having_conditions.map { |h| h[:condition] }.join(' AND ')
sql += " HAVING #{conditions}"
end
unless @order_by_clauses.empty?
orders = @order_by_clauses.map { |o| "#{o[:column]} #{o[:direction].to_s.upcase}" }
sql += " ORDER BY #{orders.join(', ')}"
end
sql += " LIMIT #{@limit_value}" if @limit_value
sql += " OFFSET #{@offset_value}" if @offset_value
sql
end
def to_s
build
end
end
# Fluent interface for building complex queries
query = QueryBuilder.new
.select('users.id', 'users.name', 'COUNT(orders.id) as order_count')
.from('users')
.join('orders', on: 'orders.user_id = users.id', type: :left)
.where('users.active = ?', true)
.where('users.created_at > ?', '2024-01-01')
.group_by('users.id', 'users.name')
.having('COUNT(orders.id) > ?', 5)
.order_by('order_count', direction: :desc)
.limit(20)
.offset(40)
puts query.to_s
Hook methods pattern uses overridable placeholder methods to extend parent class behavior without completely replacing it. This pattern appears throughout Ruby frameworks, particularly in lifecycle management and event handling.
# Hook methods pattern
class ApplicationController
def process_request(request)
before_action(request)
response = perform_action(request)
after_action(request, response)
response
rescue => error
handle_error(error, request)
ensure
cleanup(request)
end
protected
# Hook methods - meant to be overridden
def before_action(request)
# Default: no-op
end
def perform_action(request)
raise NotImplementedError, "Subclasses must implement perform_action"
end
def after_action(request, response)
# Default: no-op
end
def handle_error(error, request)
{ status: 500, body: "Internal Server Error: #{error.message}" }
end
def cleanup(request)
# Default: no-op
end
end
class UsersController < ApplicationController
def initialize
@current_user = nil
end
protected
def before_action(request)
authenticate_user(request)
log_request(request)
end
def perform_action(request)
case request[:action]
when 'index'
{ status: 200, body: list_users }
when 'show'
{ status: 200, body: find_user(request[:params][:id]) }
when 'create'
{ status: 201, body: create_user(request[:params]) }
else
{ status: 404, body: "Unknown action" }
end
end
def after_action(request, response)
log_response(response)
invalidate_cache if request[:action] == 'create'
end
def handle_error(error, request)
log_error(error, request)
case error
when AuthenticationError
{ status: 401, body: "Unauthorized" }
when ValidationError
{ status: 422, body: "Validation failed: #{error.message}" }
else
super
end
end
def cleanup(request)
close_database_connections
clear_temporary_files
end
private
def authenticate_user(request)
# Authentication logic
@current_user = User.find_by_token(request[:headers]['Authorization'])
raise AuthenticationError unless @current_user
end
def log_request(request)
puts "[REQUEST] #{request[:method]} #{request[:path]}"
end
def log_response(response)
puts "[RESPONSE] #{response[:status]}"
end
def log_error(error, request)
puts "[ERROR] #{error.class}: #{error.message} - #{request[:path]}"
end
def list_users
"User list"
end
def find_user(id)
"User #{id}"
end
def create_user(params)
"Created user"
end
def invalidate_cache
# Cache invalidation
end
def close_database_connections
# Cleanup
end
def clear_temporary_files
# Cleanup
end
end
class AuthenticationError < StandardError; end
class ValidationError < StandardError; end
controller = UsersController.new
response = controller.process_request(
method: 'GET',
path: '/users',
action: 'index',
params: {},
headers: { 'Authorization' => 'token123' }
)
Design Considerations
Choosing between simulating overloading and designing separate methods depends on the conceptual relationship between operations. If multiple signatures represent variations of the same operation, use optional parameters or keyword arguments. If they represent distinct operations, use different method names for clarity.
# When to simulate overloading vs separate methods
# GOOD: Same operation, different parameters - use one method
class ImageProcessor
def resize(image, width: nil, height: nil, scale: nil, fit: :fill)
if scale
resize_by_scale(image, scale)
elsif width && height
resize_to_dimensions(image, width, height, fit)
elsif width
resize_to_width(image, width)
elsif height
resize_to_height(image, height)
else
image # No resizing
end
end
end
# BAD: Distinct operations - should be separate methods
class DataProcessor
def process(data, format: :json, validate: false, transform: false, compress: false)
result = data
result = validate_data(result) if validate
result = transform_data(result) if transform
result = compress_data(result) if compress
convert_format(result, format)
end
end
# BETTER: Separate methods for distinct operations
class DataProcessor
def validate(data)
# Validation logic
data
end
def transform(data)
# Transformation logic
data
end
def compress(data)
# Compression logic
data
end
def convert(data, format: :json)
# Conversion logic
data
end
end
Method overriding decisions involve trade-offs between flexibility and code coupling. Deep inheritance hierarchies with extensive overriding create tight coupling and make behavior prediction difficult. Composition with strategy objects often provides better flexibility for runtime behavior changes.
Overriding protected methods requires particular care. Protected methods form part of the internal contract between parent and child classes. Changing their signatures or behavior can break subclass assumptions. Documentation should clearly specify which methods are designed for overriding and any constraints on override behavior.
The Liskov Substitution Principle guides safe overriding: subclass instances must work correctly anywhere parent instances work. This means overridden methods should accept at least as broad a range of inputs as parent methods and return values compatible with parent expectations. Violating this principle creates subtle bugs when code treats subclass instances as parent instances.
# LSP violation and fix
# BAD: Violates Liskov Substitution Principle
class Rectangle
attr_accessor :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
width * height
end
end
class Square < Rectangle
def initialize(side)
super(side, side)
end
def width=(value)
@width = value
@height = value # Violates expectation that width and height are independent
end
def height=(value)
@width = value
@height = value
end
end
# This breaks when treating Square as Rectangle
def test_rectangle(rect)
rect.width = 5
rect.height = 10
puts rect.area # Expects 50, gets 100 for Square
end
# BETTER: Avoid problematic inheritance
class Shape
def area
raise NotImplementedError
end
end
class Rectangle < Shape
attr_accessor :width, :height
def initialize(width, height)
@width = width
@height = height
end
def area
width * height
end
end
class Square < Shape
attr_accessor :side
def initialize(side)
@side = side
end
def area
side * side
end
end
Documentation becomes critical when methods are designed for overriding. Comments should specify the contract: what the method does, what parameters it accepts, what it returns, which exceptions it may raise, and any invariants that must be maintained. Documenting intended extension points helps maintainers understand which methods are safe to override.
Common Pitfalls
The most common Ruby pitfall involves expecting traditional overloading to work. Developers from Java or C++ backgrounds often define multiple methods with the same name, not realizing the last definition completely replaces all previous ones. No error or warning appears; the code simply fails to call the expected method.
# WRONG: Expecting overloading to work
class Calculator
def add(a, b)
puts "Two arguments: #{a + b}"
end
def add(a, b, c)
puts "Three arguments: #{a + b + c}"
end
end
calc = Calculator.new
calc.add(1, 2) # ArgumentError: wrong number of arguments (given 2, expected 3)
calc.add(1, 2, 3) # Works: "Three arguments: 6"
# CORRECT: Use optional parameters or splat
class Calculator
def add(*numbers)
puts "Sum: #{numbers.sum}"
end
end
calc = Calculator.new
calc.add(1, 2) # "Sum: 3"
calc.add(1, 2, 3) # "Sum: 6"
Forgetting to call super in overridden initializers breaks parent class setup. Unlike some languages that automatically call parent constructors, Ruby requires explicit super calls. Omitting super means parent instance variables never get initialized, leading to nil values and unexpected behavior.
# WRONG: Missing super in initialize
class Vehicle
attr_reader :make, :model
def initialize(make, model)
@make = make
@model = model
end
end
class Car < Vehicle
attr_reader :doors
def initialize(make, model, doors)
@doors = doors
# Missing super - @make and @model never set
end
end
car = Car.new("Toyota", "Camry", 4)
puts car.make # nil
puts car.doors # 4
# CORRECT: Call super explicitly
class Car < Vehicle
attr_reader :doors
def initialize(make, model, doors)
super(make, model) # Properly initializes parent
@doors = doors
end
end
car = Car.new("Toyota", "Camry", 4)
puts car.make # "Toyota"
puts car.doors # 4
Overriding methods without maintaining the same interface creates fragile code. Changing parameter counts, types, or return values breaks polymorphism. Code expecting the parent interface fails when receiving child instances with incompatible methods.
# WRONG: Incompatible override
class FileStorage
def save(filename, content)
File.write(filename, content)
{ success: true, path: filename }
end
end
class CloudStorage < FileStorage
def save(filename, content, bucket, region = 'us-east-1')
# Different parameter list breaks polymorphism
upload_to_cloud(bucket, region, filename, content)
end
end
def store_data(storage, filename, content)
storage.save(filename, content) # Works for FileStorage, fails for CloudStorage
end
# BETTER: Maintain compatible interface
class CloudStorage < FileStorage
attr_accessor :bucket, :region
def initialize(bucket:, region: 'us-east-1')
@bucket = bucket
@region = region
end
def save(filename, content)
upload_to_cloud(bucket, region, filename, content)
{ success: true, path: "#{bucket}/#{filename}" }
end
end
Method visibility changes in overrides can violate encapsulation. Making a private parent method public in a child exposes internals that should remain hidden. The reverse—making a public method private—breaks the parent's public contract.
# WRONG: Changing visibility in override
class Base
def public_method
"public"
end
private
def private_method
"private"
end
end
class Child < Base
def private_method # Now public - exposes internals
super + " but exposed"
end
private
def public_method # Now private - breaks contract
super
end
end
# CORRECT: Maintain visibility
class Child < Base
def public_method
super + " and extended"
end
private
def private_method
super + " and enhanced"
end
end
Excessive keyword arguments create confusion and maintenance burden. Methods accepting dozens of optional keywords become difficult to understand and test. Parameter objects or builder patterns provide better organization for complex configuration.
Calling super without parentheses passes all current method arguments, including block. This implicit behavior causes confusion when the parent method has different parameters or when modifications to arguments shouldn't propagate. Use super() with empty parentheses to pass no arguments, or super(specific, args) to control exactly what passes to the parent.
# Subtle super behavior
class Parent
def process(data, options = {})
puts "Parent received: #{data.inspect}, #{options.inspect}"
end
end
class Child < Parent
def process(data, options = {})
data = data.upcase
options[:processed] = true
super # Passes modified data and options
# Output: Parent received: "HELLO", {:processed=>true}
end
end
class AnotherChild < Parent
def process(data, options = {})
data = data.upcase
options[:processed] = true
super(data.downcase, {}) # Explicit control over arguments
# Output: Parent received: "hello", {}
end
end
Reference
Method Definition Techniques
| Technique | Description | Use Case |
|---|---|---|
| Optional parameters | Parameters with default values | Simple variation in behavior |
| Keyword arguments | Named parameters with defaults | Methods with many options |
| Splat operator | Captures remaining positional args into array | Variable argument count |
| Double splat | Captures remaining keyword args into hash | Flexible option passing |
| Block parameters | Optional code block passed with yield or call | Callback behavior |
| Parameter object | Custom object encapsulating parameters | Complex parameter sets |
| Builder pattern | Fluent interface for incremental configuration | Complex object construction |
Method Overriding Elements
| Element | Syntax | Description |
|---|---|---|
| super | super | Calls parent with all current arguments |
| super() | super() | Calls parent with no arguments |
| super with args | super(arg1, arg2) | Calls parent with specific arguments |
| Method visibility | public, protected, private | Controls method access level |
| Method lookup | obj.class.ancestors | Shows method resolution order |
Common Optional Parameter Patterns
# Boolean flag with default
def process(data, validate: true)
validate_data(data) if validate
# processing logic
end
# Enumeration with default
def format(text, style: :plain)
case style
when :plain then text
when :html then "<p>#{text}</p>"
when :markdown then "**#{text}**"
end
end
# Nil as "not provided"
def search(query, limit: nil)
results = perform_search(query)
limit ? results.first(limit) : results
end
# Hash of options
def configure(options = {})
defaults = { timeout: 30, retries: 3, verbose: false }
config = defaults.merge(options)
# use config
end
Method Resolution Order
Ruby searches for methods in this order:
- Object's singleton class
- Object's class
- Modules included in class (reverse inclusion order)
- Parent class
- Modules included in parent class
- Continues up ancestor chain
- BasicObject (root of hierarchy)
- method_missing if not found
Super Usage Decision Matrix
| Scenario | Use | Reason |
|---|---|---|
| Extend parent behavior | super at start or end of method | Adds functionality before/after parent |
| Replace parent behavior | No super call | Complete reimplementation |
| Conditionally use parent | super inside conditional | Selective parent invocation |
| Transform then delegate | Modify args, then super(args) | Process before parent execution |
| Parent needs different args | super with explicit arguments | Control parent parameter passing |
Visibility Rules for Overriding
| Parent Visibility | Child Visibility | Allowed | Notes |
|---|---|---|---|
| public | public | Yes | Standard override |
| public | protected | Avoid | Breaks public contract |
| public | private | No | Breaks public contract |
| protected | public | Avoid | Exposes internals |
| protected | protected | Yes | Maintains encapsulation |
| protected | private | Yes | Increases restriction |
| private | public | Avoid | Exposes internals |
| private | protected | Avoid | Exposes internals |
| private | private | Yes | Standard override |
Testing Override Behavior
# Verify parent method called
class TestLogger < Minitest::Test
def test_child_calls_parent
parent_called = false
Logger.class_eval do
define_method(:log_with_flag) do |message|
parent_called = true
log(message)
end
end
logger = FileLogger.new('test.log')
logger.log_with_flag("test")
assert parent_called
end
end
# Verify argument passing
def test_super_arguments
child = Child.new
child.process("data", option: true)
assert_equal "DATA", child.processed_data
assert child.options[:processed]
end
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Define same method multiple times | Last definition wins silently | Use optional/keyword arguments |
| Override without super in initialize | Parent initialization skipped | Always call super in initialize |
| Change parameter signature in override | Breaks polymorphism | Maintain compatible interface |
| Make parent public method private | Violates contract | Keep public methods public |
| Expose parent private method | Breaks encapsulation | Keep private methods private |
| Overload with boolean trap | Unclear method behavior | Use separate methods or enums |
| Deep inheritance with overrides | Tight coupling, hard to maintain | Prefer composition |
| Override without documentation | Unclear intent | Document override purpose |