Skip to content

Ruby Methods

Methods are the basic units of code organization, used to encapsulate reusable code logic. Ruby methods are powerful and flexible, supporting multiple parameter forms and advanced features. This chapter will introduce in detail the definition, calling, and advanced usage of methods in Ruby.

🎯 Method Basics

Method Definition

ruby
# Basic method definition
def greet
  puts "Hello, World!"
end

# Method with parameters
def greet_person(name)
  puts "Hello, #{name}!"
end

# Method with default parameters
def greet_with_title(name, title = "Mr./Ms.")
  puts "Hello, #{title} #{name}!"
end

# Call methods
greet                    # Hello, World!
greet_person("Zhang San")      # Hello, Zhang San!
greet_with_title("Li Si")  # Hello, Mr./Ms. Li Si!
greet_with_title("Wang Wu", "Manager")  # Hello, Manager Wang Wu!

Method Return Values

ruby
# Methods automatically return the last expression value
def add(a, b)
  a + b  # Auto return
end

# Explicit return
def multiply(a, b)
  return a * b
end

# Multiple return values
def get_name_and_age
  ["Zhang San", 25]
end

# Use parallel assignment to receive multiple return values
name, age = get_name_and_age
puts "Name: #{name}, Age: #{age}"  # Name: Zhang San, Age: 25

# Conditional return
def divide(a, b)
  return nil if b == 0
  a / b
end

puts divide(10, 2)  # 5
puts divide(10, 0)  # nil

📦 Parameter Types

Positional Parameters

ruby
# Required positional parameters
def create_user(name, age, email)
  {name: name, age: age, email: email}
end

user = create_user("Zhang San", 25, "zhangsan@example.com")

# Optional positional parameters (default values)
def greet_person(name, greeting = "Hello", punctuation = "!")
  "#{greeting}, #{name}#{punctuation}"
end

puts greet_person("Zhang San")                    # Hello, Zhang San!
puts greet_person("Li Si", "Ni Hao")             # Ni Hao, Li Si!
puts greet_person("Wang Wu", "Bonjour", ".")    # Bonjour, Wang Wu.

Keyword Parameters

ruby
# Keyword parameters (Ruby 2.0+)
def create_user(name:, age:, email: nil)
  {name: name, age: age, email: email}
end

# Must use keywords when calling
user = create_user(name: "Zhang San", age: 25)
user = create_user(age: 30, name: "Li Si", email: "lisi@example.com")

# Keyword parameters with default values
def greet_person(name:, greeting: "Hello", punctuation: "!")
  "#{greeting}, #{name}#{punctuation}"
end

puts greet_person(name: "Zhang San")  # Hello, Zhang San!

Variable Arguments

ruby
# Variable arguments (splat operator)
def sum(*numbers)
  numbers.reduce(0) { |total, n| total + n }
end

puts sum(1, 2, 3)        # 6
puts sum(1, 2, 3, 4, 5)  # 15
puts sum()               # 0

# Mix positional parameters and variable arguments
def greet_and_list(name, *items)
  puts "Hello, #{name}!"
  puts "Your items:"
  items.each { |item| puts "- #{item}" }
end

greet_and_list("Zhang San", "Apple", "Banana", "Orange")
# Hello, Zhang San!
# Your items:
# - Apple
# - Banana
# - Orange

Keyword Variable Arguments

ruby
# Keyword variable arguments (double splat operator)
def configure(**options)
  options.each { |key, value| puts "#{key}: #{value}" }
end

configure(host: "localhost", port: 3000, debug: true)
# host: localhost
# port: 3000
# debug: true

# Mix various parameter types
def complex_method(required, optional = "default", *args, **kwargs)
  puts "Required parameter: #{required}"
  puts "Optional parameter: #{optional}"
  puts "Variable arguments: #{args}"
  puts "Keyword arguments: #{kwargs}"
end

complex_method("Required", "Optional", "Extra1", "Extra2", key1: "value1", key2: "value2")
# Required parameter: Required
# Optional parameter: Optional
# Variable arguments: ["Extra1", "Extra2"]
# Keyword arguments: {:key1=>"value1", :key2=>"value2"}

Parameter Destructuring

ruby
# Array parameter destructuring
def process_coordinates((x, y))
  puts "X coordinate: #{x}, Y coordinate: #{y}"
end

point = [10, 20]
process_coordinates(point)  # X coordinate: 10, Y coordinate: 20

# Hash parameter destructuring
def process_person(name:, age:)
  puts "Name: #{name}, Age: #{age}"
end

person = {name: "Zhang San", age: 25}
process_person(**person)  # Name: Zhang San, Age: 25

🔧 Method Advanced Features

Method Aliasing

ruby
# Create alias for method
def original_method
  "Original Method"
end

alias_method :aliased_method, :original_method

puts original_method  # Original Method
puts aliased_method   # Original Method

# Use alias in class
class Calculator
  def add(a, b)
    a + b
  end
  
  alias_method :plus, :add
end

calc = Calculator.new
puts calc.add(2, 3)  # 5
puts calc.plus(2, 3) # 5

Method Visibility

ruby
class MyClass
  def public_method
    "Public Method"
  end
  
  private
  
  def private_method
    "Private Method"
  end
  
  protected
  
  def protected_method
    "Protected Method"
  end
  
  public
  
  def call_private
    private_method  # Can call private methods within class
  end
end

obj = MyClass.new
puts obj.public_method     # Public Method
puts obj.call_private      # Private Method
# obj.private_method       # Error: private method
# obj.protected_method     # Error: protected method

Method Missing Handling

ruby
class FlexibleObject
  def initialize
    @data = {}
  end
  
  # Handle calls to undefined methods
  def method_missing(method_name, *args, &block)
    if method_name.to_s.end_with?('=')
      # setter method
      key = method_name.to_s.chomp('=').to_sym
      @data[key] = args.first
    else
      # getter method
      @data[method_name]
    end
  end
  
  # Tell Ruby which methods can respond
  def respond_to_missing?(method_name, include_private = false)
    true
  end
end

obj = FlexibleObject.new
obj.name = "Zhang San"
obj.age = 25
puts obj.name  # Zhang San
puts obj.age   # 25

🔄 Blocks and Proc

Methods Accepting Blocks

ruby
# Methods accept block parameters
def with_logging
  puts "Start execution"
  yield if block_given?
  puts "Execution complete"
end

with_logging { puts "Execute specific operation" }
# Start execution
# Execute specific operation
# Execution complete

# Explicitly accept block parameter
def with_timer(&block)
  start_time = Time.now
  result = block.call
  end_time = Time.now
  puts "Execution time: #{end_time - start_time} seconds"
  result
end

result = with_timer { sleep(1); "Complete" }
puts result

Proc and Lambda

ruby
# Create Proc
my_proc = Proc.new { |x| x * 2 }
puts my_proc.call(5)  # 10

# Create Lambda
my_lambda = lambda { |x| x * 2 }
puts my_lambda.call(5)  # 10

# Shorthand Lambda syntax
my_lambda2 = ->(x) { x * 2 }
puts my_lambda2.call(5)  # 10

# Difference between Proc and Lambda
def test_return
  proc = Proc.new { return "Proc returns" }
  lambda = -> { return "Lambda returns" }
  
  proc.call  # Returns from test_return method
  lambda.call  # Returns only from lambda
  "Method ended"
end

# puts test_return  # Proc returns

Passing Blocks to Methods

ruby
# Pass block to other methods
def process_array(array, &block)
  array.map(&block)
end

numbers = [1, 2, 3, 4, 5]
squared = process_array(numbers) { |n| n * n }
puts squared.inspect  # [1, 4, 9, 16, 25]

# Use symbol shorthand
words = ["hello", "world", "ruby"]
upcased = words.map(&:upcase)
puts upcased.inspect  # ["HELLO", "WORLD", "RUBY"]

🎯 Method Practice Examples

Configuration Builder

ruby
class ConfigBuilder
  def initialize
    @config = {}
  end
  
  def method_missing(method_name, *args, &block)
    if method_name.to_s.end_with?('=')
      key = method_name.to_s.chomp('=').to_sym
      @config[key] = args.first
    elsif block_given?
      @config[method_name] = block
    elsif args.length == 1
      @config[method_name] = args.first
    elsif args.length > 1
      @config[method_name] = args
    else
      @config[method_name]
    end
  end
  
  def respond_to_missing?(method_name, include_private = false)
    true
  end
  
  def build
    @config.dup
  end
  
  def configure(&block)
    instance_eval(&block) if block_given?
    self
  end
end

# Use configuration builder
config = ConfigBuilder.new.configure do
  database_url "postgresql://localhost/myapp"
  port 3000
  debug true
  after_initialize { puts "Initialization complete" }
end.build

puts config
# {:database_url=>"postgresql://localhost/myapp", :port=>3000, :debug=>true, :after_initialize=>#<Proc:0x000000010d0a8f80>}

Chained Method Calls

ruby
class QueryBuilder
  def initialize
    @query = {}
  end
  
  def select(*fields)
    @query[:select] = fields
    self  # Return self for chaining
  end
  
  def from(table)
    @query[:from] = table
    self
  end
  
  def where(condition)
    @query[:where] = condition
    self
  end
  
  def limit(count)
    @query[:limit] = count
    self
  end
  
  def order_by(field, direction = :asc)
    @query[:order] = {field => direction}
    self
  end
  
  def build
    query_string = "SELECT "
    query_string += @query[:select] ? @query[:select].join(", ") : "*"
    query_string += " FROM #{@query[:from]}" if @query[:from]
    query_string += " WHERE #{@query[:where]}" if @query[:where]
    query_string += " ORDER BY #{@query[:order].keys.first} #{@query[:order].values.first}" if @query[:order]
    query_string += " LIMIT #{@query[:limit]}" if @query[:limit]
    query_string
  end
end

# Use chained calls
query = QueryBuilder.new
  .select(:name, :age)
  .from(:users)
  .where("age > 18")
  .order_by(:name, :desc)
  .limit(10)
  .build

puts query
# SELECT name, age FROM users WHERE age > 18 ORDER BY name desc LIMIT 10

Decorator Pattern Methods

ruby
class TextProcessor
  def initialize(text)
    @text = text
  end
  
  def process(&block)
    if block_given?
      @text = block.call(@text)
    end
    self
  end
  
  def upcase
    process { |text| text.upcase }
  end
  
  def reverse
    process { |text| text.reverse }
  end
  
  def strip
    process { |text| text.strip }
  end
  
  def replace(pattern, replacement)
    process { |text| text.gsub(pattern, replacement) }
  end
  
  def result
    @text
  end
end

# Use text processor
result = TextProcessor.new("  hello world  ")
  .strip
  .upcase
  .replace(/O/, "0")
  .reverse
  .result

puts result  # DL0W 0LLEH

🛡️ Method Security and Validation

Parameter Validation

ruby
class SafeMethods
  def self.divide(dividend, divisor)
    # Parameter type validation
    raise ArgumentError, "Dividend must be a number" unless dividend.is_a?(Numeric)
    raise ArgumentError, "Divisor must be a number" unless divisor.is_a?(Numeric)
    
    # Business logic validation
    raise ArgumentError, "Divisor cannot be zero" if divisor == 0
    
    dividend / divisor
  end
  
  def self.create_user(name, age, email = nil)
    # Validate required parameters
    raise ArgumentError, "Name cannot be empty" if name.nil? || name.empty?
    raise ArgumentError, "Age must be positive" unless age.is_a?(Integer) && age > 0
    
    # Validate optional parameters
    if email && !email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      raise ArgumentError, "Invalid email format"
    end
    
    {name: name, age: age, email: email}
  end
end

# Use safe methods
begin
  result = SafeMethods.divide(10, 2)
  puts result  # 5
  
  user = SafeMethods.create_user("Zhang San", 25, "zhangsan@example.com")
  puts user
rescue ArgumentError => e
  puts "Error: #{e.message}"
end

Method Caching

ruby
class CachedMethods
  def initialize
    @cache = {}
  end
  
  def expensive_calculation(n)
    # Simulate expensive calculation
    cache_key = "expensive_#{n}"
    
    if @cache.key?(cache_key)
      puts "Get result from cache"
      return @cache[cache_key]
    end
    
    puts "Execute calculation"
    result = (1..n).reduce(1) { |acc, i| acc * i }  # Calculate factorial
    @cache[cache_key] = result
    result
  end
  
  def clear_cache
    @cache.clear
  end
end

# Use cached methods
calculator = CachedMethods.new
puts calculator.expensive_calculation(5)  # Execute calculation, 120
puts calculator.expensive_calculation(5)  # Get result from cache, 120

🎯 Method Best Practices

1. Method Design Principles

ruby
# Good practice: Single responsibility principle
class UserManager
  def create_user(name, email)
    validate_user_data(name, email)
    user = build_user(name, email)
    save_user(user)
  end
  
  private
  
  def validate_user_data(name, email)
    raise ArgumentError, "Name cannot be empty" if name.nil? || name.empty?
    raise ArgumentError, "Invalid email format" unless valid_email?(email)
  end
  
  def build_user(name, email)
    {name: name, email: email, created_at: Time.now}
  end
  
  def save_user(user)
    # Save user logic
    puts "User saved: #{user[:name]}"
  end
  
  def valid_email?(email)
    email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
  end
end

2. Parameter Handling Best Practices

ruby
# Use keyword parameters for better readability
def create_http_request(
  method:,
  url:,
  headers: {},
  body: nil,
  timeout: 30,
  retries: 3
)
  {
    method: method.upcase,
    url: url,
    headers: headers,
    body: body,
    timeout: timeout,
    retries: retries
  }
end

# Clear when calling
request = create_http_request(
  method: "post",
  url: "https://api.example.com/users",
  headers: {"Content-Type" => "application/json"},
  body: '{"name": "Zhang San"}',
  timeout: 60
)

# Use options hash pattern
def configure_database(options = {})
  config = {
    host: options[:host] || "localhost",
    port: options[:port] || 5432,
    database: options[:database] || "myapp",
    username: options[:username] || "user",
    password: options[:password] || nil
  }
  
  # Configuration logic
  config
end

3. Error Handling

ruby
class ApiClient
  def get_user(user_id)
    # Return result or error
    response = make_request("/users/#{user_id}")
    
    case response.status
    when 200
      parse_user_data(response.body)
    when 404
      raise UserNotFoundError, "User not found: #{user_id}"
    when 403
      raise PermissionError, "Insufficient permissions"
    when 500
      raise ServerError, "Server error"
    else
      raise ApiError, "Unknown error: #{response.status}"
    end
  end
  
  private
  
  def make_request(endpoint)
    # Simulate HTTP request
    OpenStruct.new(status: 200, body: '{"id": 1, "name": "Zhang San"}')
  end
  
  def parse_user_data(json_data)
    JSON.parse(json_data)
  rescue JSON::ParserError => e
    raise DataParsingError, "Data parsing failed: #{e.message}"
  end
end

# Custom exception classes
class ApiError < StandardError; end
class UserNotFoundError < ApiError; end
class PermissionError < ApiError; end
class ServerError < ApiError; end
class DataParsingError < ApiError; end

4. Documenting Methods

ruby
class MathUtils
  # Calculate the greatest common divisor of two numbers
  # 
  # Uses Euclidean algorithm to calculate GCD
  # 
  # @param a [Integer] First number
  # @param b [Integer] Second number
  # 
  # @return [Integer] Greatest common divisor
  # 
  # @example
  #   MathUtils.gcd(48, 18)  # => 6
  #   MathUtils.gcd(17, 13)  # => 1
  # 
  # @raise [ArgumentError] Raised when parameters are not positive integers
  def self.gcd(a, b)
    # Parameter validation
    raise ArgumentError, "Parameters must be positive integers" unless a.is_a?(Integer) && b.is_a?(Integer)
    raise ArgumentError, "Parameters must be greater than 0" unless a > 0 && b > 0
    
    # Euclidean algorithm
    while b != 0
      a, b = b, a % b
    end
    a
  end
  
  # Calculate the average of an array
  # 
  # @param numbers [Array<Numeric>] Array of numbers
  # 
  # @return [Float] Average value
  # 
  # @example
  #   MathUtils.average([1, 2, 3, 4, 5])  # => 3.0
  #   MathUtils.average([10, 20, 30])     # => 20.0
  # 
  # @raise [ArgumentError] Raised when array is empty or contains non-numbers
  def self.average(numbers)
    # Parameter validation
    raise ArgumentError, "Parameter must be an array" unless numbers.is_a?(Array)
    raise ArgumentError, "Array cannot be empty" if numbers.empty?
    raise ArgumentError, "Array must contain numbers" unless numbers.all? { |n| n.is_a?(Numeric) }
    
    numbers.sum.to_f / numbers.length
  end
end

📚 Next Steps

After mastering Ruby methods, it is recommended to continue learning:

Continue your Ruby learning journey!

Content is for learning and research only.