| name | ruby-patterns |
| description | Modern Ruby idioms, design patterns, metaprogramming techniques, and best practices. Use when writing Ruby code or refactoring for clarity. |
Ruby Patterns Skill
Tier 1: Quick Reference - Common Idioms
Conditional Assignment
# Set if nil
value ||= default_value
# Set if falsy (nil or false)
value = value || default_value
# Safe navigation
user&.profile&.avatar&.url
Array and Hash Shortcuts
# Array creation
%w[apple banana orange] # ["apple", "banana", "orange"]
%i[name email age] # [:name, :email, :age]
# Hash creation
{ name: 'John', age: 30 } # Symbol keys
{ 'name' => 'John' } # String keys
# Hash access with default
hash.fetch(:key, default)
hash[:key] || default
Enumerable Shortcuts
# Transformation
array.map(&:upcase)
array.select(&:active?)
array.reject(&:empty?)
# Aggregation
array.sum
array.max
array.min
numbers.reduce(:+)
# Finding
array.find(&:valid?)
array.any?(&:present?)
array.all?(&:valid?)
String Operations
# Interpolation
"Hello #{name}!"
# Safe interpolation
"Result: %{value}" % { value: result }
# Multiline
<<~TEXT
Heredoc with indentation
removed automatically
TEXT
Block Syntax
# Single line - use braces
array.map { |x| x * 2 }
# Multi-line - use do/end
array.each do |item|
process(item)
log(item)
end
# Symbol to_proc
array.map(&:to_s)
array.select(&:even?)
Guard Clauses
def process(user)
return unless user
return unless user.active?
# Main logic here
end
Case Statements
# Traditional
case status
when 'active'
activate
when 'inactive'
deactivate
end
# With ranges
case age
when 0..17
'minor'
when 18..64
'adult'
else
'senior'
end
Tier 2: Detailed Instructions - Design Patterns
Creational Patterns
Factory Pattern:
class UserFactory
def self.create(type, attributes)
case type
when :admin
AdminUser.new(attributes)
when :member
MemberUser.new(attributes)
when :guest
GuestUser.new(attributes)
else
raise ArgumentError, "Unknown user type: #{type}"
end
end
end
# Usage
user = UserFactory.create(:admin, name: 'John', email: 'john@example.com')
Builder Pattern:
class QueryBuilder
def initialize
@conditions = []
@order = nil
@limit = nil
end
def where(condition)
@conditions << condition
self
end
def order(column)
@order = column
self
end
def limit(count)
@limit = count
self
end
def build
query = "SELECT * FROM users"
query += " WHERE #{@conditions.join(' AND ')}" if @conditions.any?
query += " ORDER BY #{@order}" if @order
query += " LIMIT #{@limit}" if @limit
query
end
end
# Usage
query = QueryBuilder.new
.where("active = true")
.where("age > 18")
.order("created_at DESC")
.limit(10)
.build
Singleton Pattern:
require 'singleton'
class Configuration
include Singleton
attr_accessor :api_key, :timeout
def initialize
@api_key = ENV['API_KEY']
@timeout = 30
end
end
# Usage
config = Configuration.instance
config.api_key = 'new_key'
Structural Patterns
Decorator Pattern:
# Simple decorator
class User
attr_accessor :name, :email
def initialize(name, email)
@name = name
@email = email
end
end
class AdminUser < SimpleDelegator
def permissions
[:read, :write, :delete, :admin]
end
def admin?
true
end
end
# Usage
user = User.new('John', 'john@example.com')
admin = AdminUser.new(user)
admin.name # Delegates to user
admin.admin? # From decorator
# Using Ruby's Forwardable
require 'forwardable'
class UserDecorator
extend Forwardable
def_delegators :@user, :name, :email
def initialize(user)
@user = user
end
def display_name
"#{@user.name} (#{@user.email})"
end
end
Adapter Pattern:
# Adapting third-party API
class LegacyPaymentGateway
def make_payment(amount, card)
# Legacy implementation
end
end
class PaymentAdapter
def initialize(gateway)
@gateway = gateway
end
def process(amount:, card_number:)
card = { number: card_number }
@gateway.make_payment(amount, card)
end
end
# Usage
legacy = LegacyPaymentGateway.new
adapter = PaymentAdapter.new(legacy)
adapter.process(amount: 100, card_number: '1234')
Composite Pattern:
class File
attr_reader :name, :size
def initialize(name, size)
@name = name
@size = size
end
def total_size
size
end
end
class Directory
attr_reader :name
def initialize(name)
@name = name
@contents = []
end
def add(item)
@contents << item
end
def total_size
@contents.sum(&:total_size)
end
end
# Usage
root = Directory.new('root')
root.add(File.new('file1.txt', 100))
subdir = Directory.new('subdir')
subdir.add(File.new('file2.txt', 200))
root.add(subdir)
root.total_size # 300
Behavioral Patterns
Strategy Pattern:
class PaymentProcessor
def initialize(strategy)
@strategy = strategy
end
def process(amount)
@strategy.process(amount)
end
end
class CreditCardStrategy
def process(amount)
puts "Processing #{amount} via credit card"
end
end
class PayPalStrategy
def process(amount)
puts "Processing #{amount} via PayPal"
end
end
# Usage
processor = PaymentProcessor.new(CreditCardStrategy.new)
processor.process(100)
processor = PaymentProcessor.new(PayPalStrategy.new)
processor.process(100)
Observer Pattern:
require 'observer'
class Order
include Observable
attr_reader :status
def initialize
@status = :pending
end
def complete!
@status = :completed
changed
notify_observers(self)
end
end
class EmailNotifier
def update(order)
puts "Sending email: Order #{order.object_id} is #{order.status}"
end
end
class SMSNotifier
def update(order)
puts "Sending SMS: Order #{order.object_id} is #{order.status}"
end
end
# Usage
order = Order.new
order.add_observer(EmailNotifier.new)
order.add_observer(SMSNotifier.new)
order.complete! # Both notifiers triggered
Command Pattern:
class Command
def execute
raise NotImplementedError
end
def undo
raise NotImplementedError
end
end
class CreateUserCommand < Command
def initialize(user_service, params)
@user_service = user_service
@params = params
@user = nil
end
def execute
@user = @user_service.create(@params)
end
def undo
@user_service.delete(@user.id) if @user
end
end
class CommandInvoker
def initialize
@history = []
end
def execute(command)
command.execute
@history << command
end
def undo
command = @history.pop
command&.undo
end
end
# Usage
invoker = CommandInvoker.new
command = CreateUserCommand.new(user_service, { name: 'John' })
invoker.execute(command)
invoker.undo # Rolls back
Metaprogramming Techniques
Dynamic Method Definition:
class Model
ATTRIBUTES = [:name, :email, :age]
ATTRIBUTES.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
end
# Usage
model = Model.new
model.name = 'John'
model.name # 'John'
Method Missing:
class DynamicFinder
def initialize(data)
@data = data
end
def method_missing(method_name, *args)
if method_name.to_s.start_with?('find_by_')
attribute = method_name.to_s.sub('find_by_', '')
@data.find { |item| item[attribute.to_sym] == args.first }
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
method_name.to_s.start_with?('find_by_') || super
end
end
# Usage
data = [
{ name: 'John', email: 'john@example.com' },
{ name: 'Jane', email: 'jane@example.com' }
]
finder = DynamicFinder.new(data)
finder.find_by_name('John') # { name: 'John', ... }
finder.find_by_email('jane@example.com') # { name: 'Jane', ... }
Class Macros (DSL):
class Validator
def self.validates(attribute, rules)
@validations ||= []
@validations << [attribute, rules]
define_method(:valid?) do
self.class.instance_variable_get(:@validations).all? do |attr, rules|
value = send(attr)
validate_rules(value, rules)
end
end
end
def validate_rules(value, rules)
rules.all? do |rule, param|
case rule
when :presence
!value.nil? && !value.empty?
when :length
value.length <= param
when :format
value.match?(param)
else
true
end
end
end
end
class User < Validator
attr_accessor :name, :email
validates :name, presence: true, length: 50
validates :email, presence: true, format: /@/
def initialize(name, email)
@name = name
@email = email
end
end
# Usage
user = User.new('John', 'john@example.com')
user.valid? # true
Module Inclusion Hooks:
module Timestampable
def self.included(base)
base.class_eval do
attr_accessor :created_at, :updated_at
define_method(:touch) do
self.updated_at = Time.now
end
end
end
end
# Using ActiveSupport::Concern for cleaner syntax
module Trackable
extend ActiveSupport::Concern
included do
attr_accessor :tracked_at
end
class_methods do
def tracking_enabled?
true
end
end
def track!
self.tracked_at = Time.now
end
end
class Model
include Timestampable
include Trackable
end
# Usage
model = Model.new
model.touch
model.track!
Tier 3: Resources & Examples
Performance Patterns
Memoization:
# Basic memoization
def expensive_calculation
@expensive_calculation ||= begin
# Expensive operation
sleep 1
'result'
end
end
# Memoization with parameters
def user_posts(user_id)
@user_posts ||= {}
@user_posts[user_id] ||= Post.where(user_id: user_id).to_a
end
# Thread-safe memoization
require 'concurrent'
class Service
def initialize
@cache = Concurrent::Map.new
end
def get(key)
@cache.compute_if_absent(key) do
expensive_operation(key)
end
end
end
Lazy Evaluation:
# Lazy enumeration for large datasets
(1..Float::INFINITY)
.lazy
.select { |n| n % 3 == 0 }
.first(10)
# Lazy file processing
File.foreach('large_file.txt').lazy
.select { |line| line.include?('ERROR') }
.map(&:strip)
.first(100)
# Custom lazy enumerator
def lazy_range(start, finish)
Enumerator.new do |yielder|
current = start
while current <= finish
yielder << current
current += 1
end
end.lazy
end
Struct for Value Objects:
# Simple value object
User = Struct.new(:name, :email, :age) do
def adult?
age >= 18
end
def to_s
"#{name} <#{email}>"
end
end
# Keyword arguments (Ruby 2.5+)
User = Struct.new(:name, :email, :age, keyword_init: true)
user = User.new(name: 'John', email: 'john@example.com', age: 30)
# Data class (Ruby 3.2+)
User = Data.define(:name, :email, :age) do
def adult?
age >= 18
end
end
Error Handling Patterns
Custom Exceptions:
class ApplicationError < StandardError; end
class ValidationError < ApplicationError; end
class NotFoundError < ApplicationError; end
class AuthenticationError < ApplicationError; end
class UserService
def create(params)
raise ValidationError, 'Name is required' if params[:name].nil?
User.create(params)
rescue ActiveRecord::RecordNotFound => e
raise NotFoundError, e.message
end
end
# Usage with rescue
begin
user_service.create(params)
rescue ValidationError => e
render json: { error: e.message }, status: 422
rescue NotFoundError => e
render json: { error: e.message }, status: 404
rescue ApplicationError => e
render json: { error: e.message }, status: 500
end
Result Object Pattern:
class Result
attr_reader :value, :error
def initialize(success, value, error = nil)
@success = success
@value = value
@error = error
end
def success?
@success
end
def failure?
!@success
end
def self.success(value)
new(true, value)
end
def self.failure(error)
new(false, nil, error)
end
def on_success(&block)
block.call(value) if success?
self
end
def on_failure(&block)
block.call(error) if failure?
self
end
end
# Usage
def create_user(params)
user = User.new(params)
if user.valid?
user.save
Result.success(user)
else
Result.failure(user.errors)
end
end
result = create_user(params)
result
.on_success { |user| send_welcome_email(user) }
.on_failure { |errors| log_errors(errors) }
Testing Patterns
Shared Examples:
RSpec.shared_examples 'a timestamped model' do
it 'has created_at' do
expect(subject).to respond_to(:created_at)
end
it 'has updated_at' do
expect(subject).to respond_to(:updated_at)
end
it 'sets timestamps on create' do
subject.save
expect(subject.created_at).to be_present
expect(subject.updated_at).to be_present
end
end
RSpec.describe User do
it_behaves_like 'a timestamped model'
end
Functional Programming Patterns
Composition:
# Function composition
add_one = ->(x) { x + 1 }
double = ->(x) { x * 2 }
square = ->(x) { x ** 2 }
# Manual composition
result = square.call(double.call(add_one.call(5))) # ((5+1)*2)^2 = 144
# Compose helper
def compose(*fns)
->(x) { fns.reverse.reduce(x) { |acc, fn| fn.call(acc) } }
end
composed = compose(square, double, add_one)
composed.call(5) # 144
Immutability:
# Frozen objects
class ImmutablePoint
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
freeze
end
def move(dx, dy)
ImmutablePoint.new(@x + dx, @y + dy)
end
end
# Frozen literals (Ruby 3+)
# frozen_string_literal: true
NAME = 'John' # Frozen by default
Additional Resources
See assets/ directory for:
idioms-cheatsheet.md- Quick reference for Ruby idiomsdesign-patterns.rb- Complete implementations of all patternsmetaprogramming-examples.rb- Advanced metaprogramming techniques
See references/ directory for:
- Style guides and best practices
- Performance optimization examples
- Testing pattern library