CrackedRuby CrackedRuby

Method Overloading and Overriding

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:

  1. Object's singleton class
  2. Object's class
  3. Modules included in class (reverse inclusion order)
  4. Parent class
  5. Modules included in parent class
  6. Continues up ancestor chain
  7. BasicObject (root of hierarchy)
  8. 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