| name | ruby-patterns |
| description | Use when writing Ruby code - provides idiomatic patterns, best practices, and community standards for maintainable Ruby |
Ruby Patterns
Expert guidance for writing idiomatic, maintainable Ruby code following best practices and community standards.
Core Ruby Idioms
Blocks, Procs, and Lambdas
Blocks - The Ruby Way of Iteration
users.each { |user| user.activate! }
users.select do |user|
user.active? && user.verified?
end
def with_timing
start = Time.now
yield
Time.now - start
end
def retry_on_failure(&block)
block.call
rescue StandardError => e
retry if retries < 3
end
Procs - Reusable Code Blocks
greeting = Proc.new { |name| "Hello, #{name}!" }
greeting.call("Alice")
greeting.call
class EventEmitter
def on(event, &handler)
@handlers ||= {}
@handlers[event] = handler
end
def emit(event, *args)
@handlers[event]&.call(*args)
end
end
Lambdas - Strict Anonymous Functions
multiply = ->(x, y) { x * y }
multiply.call(3, 4)
multiply.call(3)
class Validator
def initialize
@rules = []
end
def add_rule(&rule)
@rules << rule
end
def validate(value)
@rules.all? { |rule| rule.call(value) }
end
end
validator = Validator.new
validator.add_rule { |x| x.is_a?(String) }
validator.add_rule { |x| x.length > 3 }
Symbols vs Strings
user = { name: "Alice", role: :admin }
message = "Hello, #{user[:name]}"
:status.object_id == :status.object_id
"status".object_id == "status".object_id
def create_user(name:, email:, role: :member)
{ name: name, email: email, role: role }
end
Gems and Bundler
Gemfile Best Practices
source 'https://rubygems.org'
ruby '3.2.0'
gem 'rails', '~> 7.0'
gem 'puma', '~> 6.0'
group :development, :test do
gem 'rspec-rails', '~> 6.0'
gem 'factory_bot_rails', '~> 6.2'
gem 'faker', '~> 3.2'
end
group :development do
gem 'rubocop', '~> 1.50', require: false
gem 'rubocop-rails', require: false
gem 'bullet'
end
group :test do
gem 'simplecov', require: false
gem 'webmock', '~> 3.18'
end
gem 'devise', '4.9.2'
gem 'custom_gem', git: 'https://github.com/org/custom_gem', branch: 'main'
Bundler Commands
bundle install
bundle update rails
bundle audit
bundle outdated
bundle exec rake db:migrate
bundle exec rspec
bundle binstubs rspec-core
Style Guide (RuboCop Standards)
Code Layout
class User
def initialize(name)
@name = name
end
end
user = User.create(
name: "Alice",
email: "alice@example.com",
role: :admin
)
COLORS = [
:red,
:green,
:blue,
]
old_style = { :name => "Alice", :age => 30 }
new_style = { name: "Alice", age: 30 }
Naming Conventions
class UserAccount; end
module PaymentProcessor; end
def calculate_total_price; end
user_name = "Alice"
MAX_RETRIES = 3
API_ENDPOINT = "https://api.example.com"
def active?
@status == :active
end
def save!
raise ValidationError unless valid?
persist
end
private
def _internal_calculation
end
Method Organization
class User
VALID_ROLES = [:admin, :member, :guest].freeze
def self.find_active
where(active: true)
end
def initialize(name)
@name = name
end
def activate!
@active = true
end
def active?
@active
end
protected
def validate_role
VALID_ROLES.include?(@role)
end
private
def sanitize_name
@name.strip.downcase
end
end
Performance Patterns
Memory Optimization
users.map { |u| u["name"] }
users.map { |u| u[:name] }
ERROR_MESSAGE = "Something went wrong".freeze
TEMPLATE = <<~HTML
<div class="user">
<h1>#{name}</h1>
</div>
HTML
10.times { User.new.process }
user = User.new
10.times { user.process }
Efficient Iteration
for item in items
process(item)
end
items.each { |item| process(item) }
names = users.map(&:name)
active_users = users.select(&:active?)
inactive_users = users.reject(&:active?)
total = numbers.reduce(0, :+)
total = numbers.reduce(0) { |sum, n| sum + n }
(1..Float::INFINITY)
.lazy
.select { |n| n % 3 == 0 }
.take(10)
.to_a
String Performance
message = "Hello, " + name + "!"
message = "Hello, #{name}!"
result = ""
items.each { |item| result += item.to_s }
result = ""
items.each { |item| result << item.to_s }
result = items.map(&:to_s).join
Memoization
class Report
def total
@total ||= calculate_total
end
def valid?
return @valid if defined?(@valid)
@valid = perform_validation
end
def cached_value
return @cached_value if instance_variable_defined?(:@cached_value)
@cached_value = expensive_operation
end
end
Error Handling
Exception Hierarchy
class PaymentError < StandardError; end
class InsufficientFundsError < PaymentError; end
class InvalidCardError < PaymentError; end
class ValidationError < StandardError
attr_reader :field, :value
def initialize(field, value, message = nil)
@field = field
@value = value
super(message || "Invalid #{field}: #{value}")
end
end
raise ValidationError.new(:email, "invalid", "Email format is incorrect")
Rescue Best Practices
begin
risky_operation
rescue
handle_error
end
begin
risky_operation
rescue StandardError => e
handle_error(e)
end
begin
payment.process!
rescue InsufficientFundsError => e
notify_user("Insufficient funds")
rescue InvalidCardError => e
notify_user("Invalid card")
rescue PaymentError => e
log_error(e)
notify_admin(e)
end
result = risky_operation rescue default_value
def process_file(path)
file = File.open(path)
process(file)
ensure
file.close if file
end
def fetch_data
retries = 0
begin
api.fetch
rescue NetworkError => e
retries += 1
retry if retries < 3
raise
end
end
Fail Fast
def process_payment(amount, card)
raise ArgumentError, "Amount must be positive" if amount <= 0
raise ArgumentError, "Card required" if card.nil?
end
def calculate_discount(user, amount)
return 0 unless user.active?
return 0 if amount < 10
amount * user.discount_rate
end
Module and Class Organization
Module Mixins
module Timestampable
def touch
@updated_at = Time.now
end
def created_at
@created_at ||= Time.now
end
end
class User
include Timestampable
end
module Findable
def find_by_name(name)
all.find { |item| item.name == name }
end
end
class User
extend Findable
end
module Trackable
extend ActiveSupport::Concern
included do
before_save :update_timestamp
end
class_methods do
def recent
where("created_at > ?", 1.day.ago)
end
end
def update_timestamp
self.updated_at = Time.now
end
end
Inheritance vs Composition
class AdminUser < User
def delete_user(user)
user.destroy
end
end
class User
attr_reader :role
def initialize(role:)
@role = role
end
def can_delete_users?
role.can?(:delete_users)
end
end
class Role
def initialize(permissions)
@permissions = permissions
end
def can?(action)
@permissions.include?(action)
end
end
class Animal
def breathe
end
end
class Dog < Animal
def bark
end
end
Namespacing
module Payment
class Processor
def process(amount)
end
end
class Validator
def valid?(card)
end
end
class Error < StandardError; end
end
processor = Payment::Processor.new
processor.process(100)
module Company
module Payment
module CreditCard
class Processor
end
end
end
end
Common Patterns
Service Objects
class UserRegistration
def initialize(user_params)
@user_params = user_params
end
def call
validate!
user = create_user
send_welcome_email(user)
notify_admin(user)
user
rescue => e
handle_error(e)
nil
end
private
attr_reader :user_params
def validate!
raise ValidationError unless valid_email?
end
def create_user
User.create!(user_params)
end
def send_welcome_email(user)
UserMailer.welcome(user).deliver_later
end
def notify_admin(user)
AdminNotifier.new_user(user).notify
end
def valid_email?
user_params[:email] =~ URI::MailTo::EMAIL_REGEXP
end
def handle_error(error)
Logger.error("Registration failed: #{error.message}")
end
end
result = UserRegistration.new(params).call
Form Objects
class UserRegistrationForm
include ActiveModel::Model
attr_accessor :first_name, :last_name, :email, :password, :terms_accepted
validates :first_name, :last_name, :email, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { minimum: 8 }
validates :terms_accepted, acceptance: true
def save
return false unless valid?
user = User.new(user_attributes)
user.save!
end
private
def user_attributes
{
first_name: first_name,
last_name: last_name,
email: email,
password: password
}
end
end
form = UserRegistrationForm.new(params)
if form.save
redirect_to root_path
else
render :new, status: :unprocessable_entity
end
Decorators (Presenters)
class UserDecorator
def initialize(user)
@user = user
end
def full_name
"#{@user.first_name} #{@user.last_name}"
end
def formatted_created_at
@user.created_at.strftime("%B %d, %Y")
end
def status_badge
case @user.status
when :active then '<span class="badge-success">Active</span>'
when :inactive then '<span class="badge-warning">Inactive</span>'
else '<span class="badge-secondary">Unknown</span>'
end
end
def method_missing(method, *args, &block)
if @user.respond_to?(method)
@user.send(method, *args, &block)
else
super
end
end
def respond_to_missing?(method, include_private = false)
@user.respond_to?(method) || super
end
end
user = UserDecorator.new(User.find(params[:id]))
user.full_name
user.formatted_created_at
Query Objects
class ActiveUsersQuery
def initialize(relation = User.all)
@relation = relation
end
def call
@relation
.where(active: true)
.where("last_login_at > ?", 30.days.ago)
.order(created_at: :desc)
end
end
class UserSearchQuery
def initialize(relation = User.all)
@relation = relation
end
def call(search_params)
result = @relation
result = by_name(result, search_params[:name]) if search_params[:name]
result = by_role(result, search_params[:role]) if search_params[:role]
result = by_status(result, search_params[:status]) if search_params[:status]
result
end
private
def by_name(relation, name)
relation.where("name ILIKE ?", "%#{name}%")
end
def by_role(relation, role)
relation.where(role: role)
end
def by_status(relation, status)
relation.where(status: status)
end
end
ActiveUsersQuery.new.call
UserSearchQuery.new.call(name: "Alice", role: :admin)
Policy Objects
class UserPolicy
def initialize(user, record)
@user = user
@record = record
end
def edit?
user_is_owner? || user_is_admin?
end
def destroy?
user_is_admin? && !record_is_self?
end
def update?
edit?
end
private
attr_reader :user, :record
def user_is_owner?
@user.id == @record.id
end
def user_is_admin?
@user.role == :admin
end
def record_is_self?
@user.id == @record.id
end
end
policy = UserPolicy.new(current_user, @user)
if policy.edit?
else
end
Value Objects
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
@amount = amount.to_f
@currency = currency
end
def +(other)
raise CurrencyMismatch unless same_currency?(other)
Money.new(@amount + other.amount, @currency)
end
def -(other)
raise CurrencyMismatch unless same_currency?(other)
Money.new(@amount - other.amount, @currency)
end
def *(multiplier)
Money.new(@amount * multiplier, @currency)
end
def <=>(other)
return nil unless same_currency?(other)
@amount <=> other.amount
end
def to_s
format("%.2f %s", @amount, @currency)
end
private
def same_currency?(other)
@currency == other.currency
end
class CurrencyMismatch < StandardError; end
end
price = Money.new(10.50)
tax = Money.new(2.10)
total = price + tax
Anti-Patterns to Avoid
God Objects
class User
def authenticate; end
def send_email; end
def process_payment; end
def generate_report; end
def export_to_csv; end
end
class User
def authenticate; end
end
class UserMailer
def send_welcome_email(user); end
end
class PaymentProcessor
def process(user, amount); end
end
Callback Hell
class User
before_validation :normalize_email
after_create :send_welcome_email
after_create :create_profile
after_create :notify_admin
after_create :track_signup
after_commit :sync_to_crm
end
class UserRegistration
def call
user = User.create!(params)
send_welcome_email(user)
create_profile(user)
notify_admin(user)
track_signup(user)
sync_to_crm(user)
user
end
end
Long Parameter Lists
def create_user(first_name, last_name, email, password, role, department, manager_id)
end
def create_user(attributes)
end
class UserAttributes
attr_accessor :first_name, :last_name, :email, :password, :role, :department, :manager_id
def initialize(**attrs)
attrs.each { |key, value| send("#{key}=", value) }
end
end
Primitive Obsession
def charge_customer(customer_id, amount, currency)
end
def charge_customer(customer, money)
end
Feature Envy
class Invoice
def total
line_items.sum { |item| item.quantity * item.unit_price }
end
end
class LineItem
def total
quantity * unit_price
end
end
class Invoice
def total
line_items.sum(&:total)
end
end
Excessive Method Chaining
user.posts.published.recent.with_comments.first.comments.approved.map(&:author)
published_posts = user.posts.published.recent.with_comments
first_post = published_posts.first
return [] unless first_post
approved_comments = first_post.comments.approved
approved_comments.map(&:author)
Testing Patterns
RSpec.describe UserRegistration do
describe '#call' do
let(:valid_params) { { email: 'test@example.com', password: 'password123' } }
context 'with valid parameters' do
it 'creates a user' do
expect { described_class.new(valid_params).call }
.to change(User, :count).by(1)
end
it 'sends welcome email' do
expect(UserMailer).to receive(:welcome).and_call_original
described_class.new(valid_params).call
end
end
context 'with invalid email' do
let(:invalid_params) { valid_params.merge(email: 'invalid') }
it 'does not create user' do
expect { described_class.new(invalid_params).call }
.not_to change(User, :count)
end
end
end
end
Key Principles
- Follow the Ruby Way: Embrace blocks, duck typing, and metaprogramming judiciously
- Keep It Simple: Prefer clarity over cleverness
- Single Responsibility: Each class/method should have one reason to change
- Use Descriptive Names: Code should read like natural language
- Test Your Code: Write tests first with RSpec or Minitest
- Follow Community Standards: Use RuboCop and follow the Ruby Style Guide
- Optimize Wisely: Profile first, optimize bottlenecks only
- Handle Errors Gracefully: Use specific exceptions and fail fast
- Document Public APIs: Use YARD or RDoc for library code
- Keep Dependencies Updated: Regularly update gems and fix security issues
Resources