Ruby Patterns
When to Use
Writing or reviewing idiomatic Ruby: choosing between blocks and lambdas, composing behavior with modules, or modeling small value types.
Core Patterns
Blocks, Procs, and Lambdas
# Block — the common case, implicit, no object until captured
[1, 2, 3].each { |n| puts n }
# Proc — lenient arity, `return` exits the enclosing method
adder = Proc.new { |a, b| a + b }
adder.call(1, 2, 3) # extra arg silently ignored
# Lambda — strict arity, `return` exits only the lambda itself
adder = ->(a, b) { a + b }
adder.call(1, 2) # => 3
adder.call(1) # ArgumentError — lambdas enforce arity
Default to lambdas for anything stored and called later — the strict arity and local return behavior avoid a class of subtle bugs procs introduce.
Modules: include, extend, prepend
module Trackable
def track(event)
Analytics.record(self.class.name, event)
end
end
class Order
include Trackable # adds as instance methods
end
class Report
extend Trackable # adds as class/singleton methods
end
module Loud
def greet
super.upcase
end
end
class Greeter
prepend Loud # inserts Loud ABOVE Greeter in the ancestor chain
def greet = "hello"
end
Greeter.new.greet # => "HELLO" — Loud#greet runs first, calls super
Duck Typing Over Type Checks
# BAD: rigid, breaks polymorphism
def process(payment)
raise TypeError unless payment.is_a?(CreditCardPayment)
payment.charge
end
# GOOD: trust the interface, not the class
def process(payment)
payment.charge # works for CreditCardPayment, PayPalPayment, anything with #charge
end
Enumerable Over Manual Loops
# BAD
total = 0
items.each { |item| total += item.price if item.in_stock? }
# GOOD — declarative, composes
total = items.select(&:in_stock?).sum(&:price)
active_emails = users.filter_map { |u| u.email if u.active? }
Keyword Arguments for Multi-Parameter Methods
# BAD: positional args, easy to swap by mistake
def create_order(customer, total, currency, priority)
end
# GOOD: self-documenting call sites, order-independent
def create_order(customer:, total:, currency: "USD", priority: :normal)
end
create_order(customer: user, total: 4200, currency: "EUR")
Value Objects with Struct / Data.define
# Data.define (Ruby 3.2+) — immutable value object, minimal boilerplate
Money = Data.define(:cents, :currency) do
def add(other)
raise ArgumentError, "currency mismatch" unless currency == other.currency
Money.new(cents: cents + other.cents, currency:)
end
end
price = Money.new(cents: 1500, currency: "USD")
Checklist
- Lambdas used for stored/passed-around callables; blocks for immediate iteration
-
Data.define/Structused for small immutable value objects instead of ad-hoc hashes - Keyword arguments used once a method takes 3+ parameters
- Enumerable methods (
map,select,sum,filter_map) preferred over manualeach+ accumulator -
method_missingavoided unless building a genuine proxy/DSL — and paired withrespond_to_missing?
Anti-Patterns
# BAD: method_missing as a lazy alternative to defining real methods
class Config
def method_missing(name, *args)
@data[name]
end
end
# GOOD: explicit, discoverable, works with respond_to?
class Config
def initialize(data) = @data = data
def [](key) = @data[key]
end
# BAD: mutating an argument passed by reference
def add_tax!(order)
order.total += order.total * 0.1
end
# GOOD: return a new value, keep call sites predictable
def with_tax(order)
order.with(total: order.total * 1.1)
end
Quick Reference
| Need | Use |
|---|---|
| Store a callable for later | lambda / ->() {} |
| Immediate iteration | block (each, map) |
| Add behavior to instances | include |
| Add behavior to the class itself | extend |
Override with access to original via super |
prepend |
| Small immutable value object | Data.define |
See Also
skills/ruby-ecosystem/rails-patterns.mdskills/ruby-ecosystem/ruby-testing.md