Ruby patterns are reusable solutions to common programming problems. Understanding these patterns will make your Rails code more maintainable, readable, and efficient. Let's explore the most important patterns for Rails developers.
Object-Oriented Patterns
1. Service Objects
Service objects encapsulate business logic that doesn't naturally fit in models or controllers.
# app/services/user_registration_service.rb
class UserRegistrationService
attr_reader :user, :errors
def initialize(user_params)
@user_params = user_params
@errors = []
end
def call
create_user
send_welcome_email if user.persisted?
create_default_preferences if user.persisted?
user.persisted?
end
private
attr_reader :user_params
def create_user
@user = User.new(user_params)
unless @user.save
@errors.concat(@user.errors.full_messages)
end
end
def send_welcome_email
UserMailer.welcome_email(@user).deliver_later
rescue => e
@errors << "Failed to send welcome email: #{e.message}"
end
def create_default_preferences
@user.create_preference(
theme: 'light',
email_notifications: true
)
end
end
Usage in controller:
```ruby
class UsersController < ApplicationController
def create
service = UserRegistrationService.new(user_params)
if service.call
redirect_to service.user, notice: 'Welcome!'
else
@user = service.user
@errors = service.errors
render :new
end
end
end
```
2. Form Objects
Form objects handle complex forms that span multiple models.
# app/forms/order_form.rb
class OrderForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :customer_name, :string
attribute :customer_email, :string
attribute :product_id, :integer
attribute :quantity, :integer
attribute :delivery_date, :date
validates :customer_name, :customer_email, presence: true
validates :product_id, :quantity, presence: true, numericality: { greater_than: 0 }
validates :customer_email, format: { with: URI::MailTo::EMAIL_REGEXP }
def save
return false unless valid?
ActiveRecord::Base.transaction do
create_customer
create_order
update_inventory
end
true
rescue => e
errors.add(:base, "Failed to create order: #{e.message}")
false
end
def customer
@customer
end
def order
@order
end
private
def create_customer
@customer = Customer.find_or_create_by(email: customer_email) do |c|
c.name = customer_name
end
end
def create_order
@order = @customer.orders.create!(
product_id: product_id,
quantity: quantity,
delivery_date: delivery_date
)
end
def update_inventory
product = Product.find(product_id)
product.decrement!(:stock_quantity, quantity)
end
end
3. Query Objects
Query objects encapsulate complex database queries.
# app/queries/user_search_query.rb
class UserSearchQuery
def initialize(relation = User.all)
@relation = relation
end
def search(params = {})
@relation = filter_by_name(params[:name]) if params[:name].present?
@relation = filter_by_email(params[:email]) if params[:email].present?
@relation = filter_by_role(params[:role]) if params[:role].present?
@relation = filter_by_status(params[:status]) if params[:status].present?
@relation = filter_by_created_date(params[:created_after], params[:created_before])
@relation
end
private
def filter_by_name(name)
@relation.where("name ILIKE ?", "%#{name}%")
end
def filter_by_email(email)
@relation.where("email ILIKE ?", "%#{email}%")
end
def filter_by_role(role)
@relation.where(role: role)
end
def filter_by_status(status)
case status
when 'active'
@relation.where(active: true)
when 'inactive'
@relation.where(active: false)
else
@relation
end
end
def filter_by_created_date(after_date, before_date)
relation = @relation
relation = relation.where("created_at >= ?", after_date) if after_date.present?
relation = relation.where("created_at <= ?", before_date) if before_date.present?
relation
end
end
Usage:
```ruby
In controller
def index
@users = UserSearchQuery.new.search(search_params)
.page(params[:page])
.per(20)
end
```
Functional Patterns
4. Command Pattern
Encapsulate requests as objects, allowing you to parameterize and queue operations.
# app/commands/base_command.rb
class BaseCommand
def self.call(*args)
new(*args).call
end
def call
raise NotImplementedError, "Subclasses must implement #call"
end
end
# app/commands/send_notification_command.rb
class SendNotificationCommand < BaseCommand
def initialize(user, message, type = :email)
@user = user
@message = message
@type = type
end
def call
case @type
when :email
send_email
when :sms
send_sms
when :push
send_push_notification
else
raise ArgumentError, "Unknown notification type: #{@type}"
end
end
private
def send_email
UserMailer.notification(@user, @message).deliver_now
end
def send_sms
SmsService.new.send(@user.phone, @message)
end
def send_push_notification
PushNotificationService.new.send(@user.device_token, @message)
end
end
Usage:
```ruby
Send different types of notifications
SendNotificationCommand.call(user, "Your order is ready!", :email)
SendNotificationCommand.call(user, "Urgent: Payment required", :sms)
```
5. Strategy Pattern
Define a family of algorithms and make them interchangeable.
# app/strategies/pricing_strategy.rb
module PricingStrategy
class Base
def initialize(order)
@order = order
end
def calculate
raise NotImplementedError
end
protected
attr_reader :order
end
class RegularPricing < Base
def calculate
order.items.sum { |item| item.price * item.quantity }
end
end
class BulkPricing < Base
def calculate
total = order.items.sum { |item| item.price * item.quantity }
total > 1000 ? total * 0.9 : total
end
end
class VipPricing < Base
def calculate
total = order.items.sum { |item| item.price * item.quantity }
total * 0.8 # 20% discount for VIP customers
end
end
end
# app/models/order.rb
class Order < ApplicationRecord
belongs_to :customer
has_many :items
def total_price
pricing_strategy.calculate
end
private
def pricing_strategy
case customer.tier
when 'vip'
PricingStrategy::VipPricing.new(self)
when 'bulk'
PricingStrategy::BulkPricing.new(self)
else
PricingStrategy::RegularPricing.new(self)
end
end
end
6. Decorator Pattern
Add behavior to objects without altering their structure.
# app/decorators/user_decorator.rb
class UserDecorator < SimpleDelegator
def display_name
"#{first_name} #{last_name}"
end
def avatar_url(size = :medium)
if avatar.present?
avatar.variant(resize_to_limit: [avatar_size(size), nil])
else
default_avatar_url
end
end
def status_badge
css_class = active? ? "badge-success" : "badge-secondary"
"<span class='badge #{css_class}'>#{status_text}</span>".html_safe
end
def membership_duration
return "New member" unless created_at
years = ((Time.current - created_at) / 1.year).floor
return "Member for #{years} year#{'s' if years != 1}" if years > 0
months = ((Time.current - created_at) / 1.month).floor
"Member for #{months} month#{'s' if months != 1}"
end
private
def avatar_size(size)
case size
when :small then 50
when :medium then 100
when :large then 200
else 100
end
end
def default_avatar_url
"https://via.placeholder.com/#{avatar_size(:medium)}/cccccc/ffffff?text=#{initials}"
end
def initials
[first_name, last_name].map(&:first).join.upcase
end
def status_text
active? ? "Active" : "Inactive"
end
end
Usage:
```ruby
In controller or view
decorateduser = UserDecorator.new(@user)
decorateduser.displayname
decorateduser.status_badge
```
Rails-Specific Patterns
7. Concerns Pattern
Share common functionality across multiple models or controllers.
# app/models/concerns/trackable.rb
module Trackable
extend ActiveSupport::Concern
included do
scope :recent, -> { where('created_at > ?', 1.week.ago) }
scope :old, -> { where('created_at < ?', 1.month.ago) }
after_create :track_creation
after_update :track_update
end
class_methods do
def most_active(limit = 10)
joins(:activities)
.group('activities.trackable_id')
.order('COUNT(activities.id) DESC')
.limit(limit)
end
end
def track_activity(action, metadata = {})
activities.create!(
action: action,
metadata: metadata,
occurred_at: Time.current
)
end
def activity_count
activities.count
end
def last_activity
activities.order(:occurred_at).last
end
private
def track_creation
track_activity('created')
end
def track_update
track_activity('updated', changes: previous_changes)
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Trackable
# Now has access to all Trackable functionality
end
# app/models/user.rb
class User < ApplicationRecord
include Trackable
# Also has access to all Trackable functionality
end
8. Builder Pattern
Construct complex objects step by step.
# app/builders/email_builder.rb
class EmailBuilder
def initialize
@email = {}
end
def to(recipient)
@email[:to] = recipient
self
end
def from(sender)
@email[:from] = sender
self
end
def subject(subject)
@email[:subject] = subject
self
end
def body(content)
@email[:body] = content
self
end
def template(template_name, variables = {})
@email[:template] = template_name
@email[:variables] = variables
self
end
def priority(level)
@email[:priority] = level
self
end
def schedule_for(datetime)
@email[:scheduled_for] = datetime
self
end
def build
validate_required_fields
Email.new(@email)
end
def send!
email = build
EmailService.send(email)
end
private
def validate_required_fields
required_fields = [:to, :subject]
missing_fields = required_fields.select { |field| @email[field].blank? }
if missing_fields.any?
raise ArgumentError, "Missing required fields: #{missing_fields.join(', ')}"
end
end
end
Usage:
```ruby
Fluent interface for building emails
EmailBuilder.new
.to('[email protected]')
.from('[email protected]')
.subject('Welcome to our app!')
.template('welcome_email', user: @user)
.priority(:high)
.send!
Or schedule for later
EmailBuilder.new
.to('[email protected]')
.subject('Weekly newsletter')
.template('newsletter')
.schedulefor(1.week.fromnow)
.send!
```
9. Repository Pattern
Encapsulate data access logic and provide a more object-oriented view of the persistence layer.
# app/repositories/user_repository.rb
class UserRepository
def self.find_active_users
User.where(active: true)
end
def self.find_by_role(role)
User.where(role: role)
end
def self.find_recent_signups(days = 7)
User.where('created_at > ?', days.days.ago)
end
def self.search_by_name_or_email(query)
User.where(
'name ILIKE :query OR email ILIKE :query',
query: "%#{query}%"
)
end
def self.find_with_posts
User.includes(:posts).where.not(posts: { id: nil })
end
def self.top_contributors(limit = 10)
User.joins(:posts)
.group('users.id')
.order('COUNT(posts.id) DESC')
.limit(limit)
end
end
10. Null Object Pattern
Provide default behavior for nil objects.
# app/models/null_user.rb
class NullUser
def name
"Guest User"
end
def email
nil
end
def admin?
false
end
def avatar
nil
end
def posts
Post.none
end
def display_name
"Guest"
end
def persisted?
false
end
def to_param
nil
end
end
# app/models/user.rb
class User < ApplicationRecord
def self.find_or_null(id)
find(id)
rescue ActiveRecord::RecordNotFound
NullUser.new
end
end
Usage:
```ruby
In controller - no need to check for nil
def show
@user = User.findornull(params[:id])
# @user.name will always work, returning "Guest User" for null object
end
In view - no nil checks needed
<%= @user.name %> # Safe even if user not found
```
Best Practices for Ruby Patterns
1. Choose the Right Pattern
- Use Service Objects for complex business logic
- Use Form Objects for multi-model forms
- Use Query Objects for complex database queries
- Use Decorators for presentation logic
2. Keep It Simple
# Good - Simple and focused
class UserCreationService
def initialize(params)
@params = params
end
def call
User.create(@params)
end
end
# Avoid - Over-engineered
class UserCreationServiceFactoryBuilder
# Too complex for simple user creation
end
3. Test Your Patterns
# test/services/user_registration_service_test.rb
class UserRegistrationServiceTest < ActiveSupport::TestCase
test "successfully creates user and sends welcome email" do
params = { name: "John", email: "[email protected]" }
service = UserRegistrationService.new(params)
assert service.call
assert service.user.persisted?
assert_emails 1
end
test "handles user creation failure" do
params = { name: "", email: "invalid" }
service = UserRegistrationService.new(params)
assert_not service.call
assert service.errors.any?
end
end
Understanding and applying these Ruby patterns will help you write more maintainable, testable, and organized Rails applications. Start with the simpler patterns like Service Objects and gradually introduce more complex ones as your application grows.