← Back to Contents

Chapter 3: Methods & OOP

Methods, closures, classes & modules

1. Method Definitions

Ruby methods are defined with def..end. They implicitly return the value of their last expression.

# Basic method
def greet(name)
  "Hello, #{name}!"
end

greet("Alice")  # => "Hello, Alice!"

# Default parameters
def connect(host, port: 3000, ssl: false)
  "#{ssl ? 'https' : 'http'}://#{host}:#{port}"
end

connect("localhost")              # => "http://localhost:3000"
connect("api.com", port: 443, ssl: true)  # => "https://api.com:443"

# Splat operator for variable arguments
def sum(*numbers)
  numbers.reduce(0, :+)
end

sum(1, 2, 3, 4)  # => 10

# Double splat for keyword arguments
def create_user(**attrs)
  puts attrs.inspect
end

create_user(name: "Alice", age: 30, role: :admin)
# => {:name=>"Alice", :age=>30, :role=>:admin}

Naming Conventions

# snake_case for method names
def calculate_total(items)
  items.sum { |item| item[:price] }
end

# Predicate methods end with ?
def valid?(email)
  email.include?("@")
end

# Dangerous/mutating methods end with !
name = "hello"
name.upcase   # => "HELLO" (returns new string)
name.upcase!  # => "HELLO" (modifies in place)

# Setter methods end with =
class User
  def name=(new_name)
    @name = new_name
  end
end

Convention: Methods ending in ? return a boolean, methods ending in ! perform a destructive or dangerous operation, and methods ending in = are setters. These are conventions, not enforced by the language.

πŸ”„ Comparison with other languages

Ruby's implicit return is similar to Rust and Kotlin. Keyword arguments with defaults behave like Python's. The *args and **kwargs splat operators mirror Python's *args and **kwargs exactly.

2. Blocks, Procs & Lambdas

Blocks are Ruby's most distinctive feature — anonymous chunks of code that can be passed to methods. They're the foundation of Ruby's elegant iterator patterns.

Blocks

# Two syntax styles for blocks
[1, 2, 3].each { |n| puts n }           # single-line: use { }

[1, 2, 3].each do |n|                    # multi-line: use do..end
  result = n * 10
  puts result
end

# yield calls the block passed to a method
def with_logging
  puts "[START]"
  result = yield
  puts "[END]"
  result
end

with_logging { 2 + 2 }
# [START]
# [END]
# => 4

# yield with arguments
def transform(value)
  yield(value) if block_given?
end

transform(5) { |n| n ** 2 }  # => 25
transform(5)                  # => nil (no block given)

Procs & Lambdas

# Proc: a stored block
square = Proc.new { |n| n ** 2 }
square.call(5)  # => 25
square.(5)      # => 25 (shorthand)
square[5]       # => 25 (another shorthand)

# Lambda: a stricter Proc
multiply = ->(a, b) { a * b }
multiply.call(3, 4)  # => 12
multiply.(3, 4)      # => 12

# Key differences between Proc and Lambda
# 1. Argument checking
lenient = Proc.new { |a, b| "#{a}, #{b}" }
lenient.call(1)        # => "1, " (missing arg becomes nil)

strict = ->(a, b) { "#{a}, #{b}" }
# strict.call(1)       # => ArgumentError: wrong number of arguments

# 2. Return behavior
def proc_return
  p = Proc.new { return "from proc" }
  p.call
  "after proc"  # never reached
end

def lambda_return
  l = -> { return "from lambda" }
  l.call
  "after lambda"  # this IS reached
end

proc_return    # => "from proc"
lambda_return  # => "after lambda"

The &block Parameter

# Capture a block as a Proc
def apply_twice(&block)
  block.call + block.call
end

apply_twice { 10 }  # => 20

# Convert a symbol to a block with &
["hello", "world"].map(&:upcase)  # => ["HELLO", "WORLD"]
[1, 2, 3, 4].select(&:even?)     # => [2, 4]
[1, nil, 2, nil].select(&:nil?)   # => [nil, nil]

# Pass a method as a block
def double(n)
  n * 2
end

[1, 2, 3].map(&method(:double))  # => [2, 4, 6]

Rule of thumb: Use blocks for simple iteration and callbacks. Use lambdas when you need a callable with strict argument checking. Use Procs rarely — their loose argument handling and non-local return can be surprising.

3. Classes

class Animal
  attr_reader :name
  attr_accessor :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  def speak
    raise NotImplementedError, "Subclass must implement #speak"
  end

  def to_s
    "#{@name} (age: #{@age})"
  end
end

class Dog < Animal
  attr_reader :breed

  def initialize(name, age, breed)
    super(name, age)
    @breed = breed
  end

  def speak
    "Woof!"
  end

  def fetch(item)
    "#{@name} fetches the #{item}!"
  end
end

dog = Dog.new("Rex", 3, "Labrador")
puts dog.name    # => "Rex"
puts dog.speak   # => "Woof!"
puts dog.fetch("ball")  # => "Rex fetches the ball!"
puts dog         # => "Rex (age: 3)"

Access Control

class BankAccount
  def initialize(owner, balance)
    @owner = owner
    @balance = balance
  end

  def deposit(amount)
    validate_amount(amount)
    @balance += amount
    log_transaction("deposit", amount)
    @balance
  end

  def withdraw(amount)
    validate_amount(amount)
    raise "Insufficient funds" if amount > @balance
    @balance -= amount
    log_transaction("withdrawal", amount)
    @balance
  end

  def <=>(other)
    @balance <=> other.balance
  end

  protected

  def balance
    @balance
  end

  private

  def validate_amount(amount)
    raise ArgumentError, "Amount must be positive" unless amount > 0
  end

  def log_transaction(type, amount)
    puts "[LOG] #{type}: $#{amount} | Balance: $#{@balance}"
  end
end

account = BankAccount.new("Alice", 1000)
account.deposit(500)     # => 1500
account.withdraw(200)    # => 1300
# account.balance        # => NoMethodError (protected)
# account.validate_amount(10)  # => NoMethodError (private)

Class Methods & Attributes

class Configuration
  @@instances = 0

  attr_accessor :debug, :log_level

  def initialize
    @@instances += 1
    @debug = false
    @log_level = :info
  end

  def self.instance_count
    @@instances
  end

  def self.default
    config = new
    config.debug = false
    config.log_level = :warn
    config
  end
end

c1 = Configuration.new
c2 = Configuration.default
Configuration.instance_count  # => 2

πŸ”„ OOP comparison

Feature Ruby Python Java
Inheritance class Dog < Animal class Dog(Animal) class Dog extends Animal
Constructor initialize __init__ Same as class name
Self reference self (implicit) self (explicit) this (implicit)
Multiple inheritance Mixins (modules) Yes (MRO) Interfaces only

4. Modules & Mixins

Ruby doesn't support multiple inheritance, but modules provide a powerful alternative. Modules serve two purposes: namespacing and mixins.

Namespacing

module Payments
  class CreditCard
    def charge(amount)
      puts "Charging $#{amount} to credit card"
    end
  end

  class PayPal
    def charge(amount)
      puts "Charging $#{amount} via PayPal"
    end
  end
end

cc = Payments::CreditCard.new
cc.charge(99.99)

Mixins with include / extend / prepend

module Loggable
  def log(message)
    puts "[#{self.class}] #{message}"
  end
end

module Serializable
  def to_json
    instance_variables.each_with_object({}) do |var, hash|
      hash[var.to_s.delete("@")] = instance_variable_get(var)
    end.to_json
  end
end

class User
  include Loggable       # adds instance methods
  include Serializable

  attr_accessor :name, :email

  def initialize(name, email)
    @name = name
    @email = email
    log("User created: #{name}")
  end
end

user = User.new("Alice", "alice@example.com")
# => [User] User created: Alice

include vs extend vs prepend

module Greetable
  def greet
    "Hello from #{self.class}!"
  end
end

module ClassGreeting
  def description
    "I am the #{name} class"
  end
end

module Auditable
  def save
    puts "Audit: saving record..."
    super
    puts "Audit: record saved."
  end
end

class Product
  include Greetable      # instance methods
  extend ClassGreeting   # class methods
  prepend Auditable      # inserts BEFORE the class in lookup chain

  def save
    puts "Product saved to database"
  end
end

Product.new.greet        # => "Hello from Product!"
Product.description      # => "I am the Product class"
Product.new.save
# Audit: saving record...
# Product saved to database
# Audit: record saved.

Key difference: include adds methods as instance methods. extend adds them as class methods. prepend inserts the module before the class in the method lookup chain, making it ideal for wrapping/decorating existing methods.

Comparable & Enumerable

# Comparable: get <, <=, ==, >=, >, between? for free
class Temperature
  include Comparable

  attr_reader :degrees

  def initialize(degrees)
    @degrees = degrees
  end

  def <=>(other)
    @degrees <=> other.degrees
  end
end

temps = [Temperature.new(30), Temperature.new(20), Temperature.new(25)]
temps.sort.map(&:degrees)  # => [20, 25, 30]
Temperature.new(25).between?(Temperature.new(20), Temperature.new(30))  # => true

# Enumerable: get map, select, reduce, min, max, sort, etc. for free
class NumberSet
  include Enumerable

  def initialize(*numbers)
    @data = numbers
  end

  def each(&block)
    @data.each(&block)
  end
end

set = NumberSet.new(5, 3, 8, 1, 9)
set.sort           # => [1, 3, 5, 8, 9]
set.select(&:odd?) # => [5, 3, 1, 9]
set.min            # => 1
set.reduce(:+)     # => 26

5. Built-in Modules

Ruby's standard library provides several powerful modules that you'll use constantly.

Enumerable Highlights

numbers = [4, 8, 15, 16, 23, 42]

numbers.each_slice(2).to_a     # => [[4, 8], [15, 16], [23, 42]]
numbers.each_cons(3).to_a      # => [[4, 8, 15], [8, 15, 16], ...]
numbers.flat_map { |n| [n, -n] }  # => [4, -4, 8, -8, ...]
numbers.zip(["a", "b", "c"])   # => [[4,"a"], [8,"b"], [15,"c"]]
numbers.partition(&:even?)     # => [[4, 8, 16, 42], [15, 23]]
numbers.group_by { |n| n > 20 ? :high : :low }
# => {:low=>[4, 8, 15, 16], :high=>[23, 42]}

numbers.tally                  # counts occurrences (Ruby 2.7+)
%w[a b a c b a].tally          # => {"a"=>3, "b"=>2, "c"=>1}

numbers.filter_map { |n| n * 2 if n.even? }  # => [8, 16, 32, 84]
numbers.sum                    # => 108
numbers.minmax                 # => [4, 42]
numbers.any? { |n| n > 40 }   # => true
numbers.all? { |n| n > 0 }    # => true
numbers.none? { |n| n < 0 }   # => true
numbers.count(&:even?)         # => 4

Kernel Methods

# Common Kernel methods available everywhere
puts "Hello"          # print with newline
print "Hi "           # print without newline
p [1, 2, 3]           # inspect output: [1, 2, 3]
pp({a: {b: {c: 1}}})  # pretty-print

# Type conversion
Integer("42")     # => 42
Float("3.14")     # => 3.14
String(42)        # => "42"
Array(nil)        # => []
Array(42)         # => [42]
Array([1, 2])     # => [1, 2]

# Open a file, URI, or process
open("file.txt") { |f| f.read }

# System commands
system("ls -la")        # returns true/false
output = `ls -la`       # captures output as string
result = %x(echo hello) # same as backticks

πŸ”„ Comparison with other languages

Ruby's Enumerable module is one of the richest collection APIs in any language. Methods like tally, filter_map, and each_cons have no direct equivalents in Python or JavaScript without additional libraries.

6. Struct & Data

Ruby provides lightweight alternatives to full classes for simple data objects.

Struct

Point = Struct.new(:x, :y)

p1 = Point.new(3, 4)
p1.x         # => 3
p1.y         # => 4
p1.to_a      # => [3, 4]

# Structs support equality by value
p2 = Point.new(3, 4)
p1 == p2     # => true

# Add methods to Struct
Point = Struct.new(:x, :y) do
  def distance_to(other)
    Math.sqrt((x - other.x) ** 2 + (y - other.y) ** 2)
  end

  def to_s
    "(#{x}, #{y})"
  end
end

origin = Point.new(0, 0)
point = Point.new(3, 4)
point.distance_to(origin)  # => 5.0

# Struct with keyword_init
Config = Struct.new(:host, :port, :ssl, keyword_init: true)
cfg = Config.new(host: "localhost", port: 3000, ssl: false)

Data Class (Ruby 3.2+)

# Data creates immutable value objects
Coordinate = Data.define(:lat, :lng)

tokyo = Coordinate.new(lat: 35.6762, lng: 139.6503)
tokyo.lat    # => 35.6762
tokyo.lng    # => 139.6503
# tokyo.lat = 0  # => NoMethodError (immutable!)

# Value equality
paris = Coordinate.new(lat: 48.8566, lng: 2.3522)
tokyo2 = Coordinate.new(lat: 35.6762, lng: 139.6503)
tokyo == tokyo2  # => true
tokyo == paris   # => false

# Pattern matching works naturally
case tokyo
in Coordinate[lat: (30..40), lng:]
  puts "Located in mid-latitudes, longitude: #{lng}"
end

OpenStruct

require "ostruct"

user = OpenStruct.new(name: "Alice", age: 30)
user.name      # => "Alice"
user.email = "alice@example.com"  # dynamically add attributes
user.email     # => "alice@example.com"

# Useful for quick prototyping, but slower than Struct/Data
# and lacks type safety. Prefer Struct or Data for production code.

When to use which: Use Data (Ruby 3.2+) for immutable value objects. Use Struct for mutable lightweight data containers. Use OpenStruct only for quick prototyping. Use a full class when you need complex behavior or validations.

πŸ”„ Lightweight data objects comparison

Language Mutable Immutable
Ruby Struct Data
Python @dataclass @dataclass(frozen=True) / NamedTuple
Kotlin data class (var) data class (val)
Java POJO record

7. Chapter Summary

πŸ”§ Methods

Defined with def..end. Implicit return. Default params, keyword args, splat * and double-splat **. Convention: ? for predicates, ! for mutators.

πŸ“¦ Blocks & Closures

Blocks via { } or do..end. yield to invoke. Lambdas are strict, Procs are lenient. &:method shorthand for common patterns.

πŸ—οΈ Classes

initialize constructor, attr_accessor/reader/writer. Single inheritance with <. public/protected/private access control.

🧩 Modules & Mixins

include for instance methods, extend for class methods, prepend for method wrapping. Namespacing with Module::Class.

⚑ Enumerable & Comparable

Include Enumerable + define each to get 50+ collection methods. Include Comparable + define <=> for sorting and comparison.

πŸ“‹ Struct & Data

Struct for mutable data objects. Data (Ruby 3.2+) for immutable value objects. OpenStruct for quick prototyping only.