| name | Ruby OOP Patterns |
| description | Comprehensive guide to Object-Oriented Programming in Ruby and Rails covering classes, modules, design patterns, SOLID principles, and modern Ruby 3.x features |
| version | 1.0.0 |
Ruby OOP Patterns
Complete guide to Object-Oriented Programming in Ruby and Ruby on Rails, from fundamentals to advanced patterns.
Ruby OOP Fundamentals
Classes and Objects
# Basic class definition
class User
# Class variables (shared across all instances)
@@user_count = 0
# Constants
DEFAULT_ROLE = 'member'
# Constructor
def initialize(name, email)
@name = name # Instance variable
@email = email
@role = DEFAULT_ROLE
@@user_count += 1
end
# Instance method
def greet
"Hello, I'm #{@name}"
end
# Class method (alternative syntax)
def self.count
@@user_count
end
# Class method (using class << self)
class << self
def reset_count
@@user_count = 0
end
end
end
user = User.new('Alice', 'alice@example.com')
user.greet # => "Hello, I'm Alice"
User.count # => 1
Attribute Accessors
class Product
# Read and write
attr_accessor :name, :price
# Read only
attr_reader :created_at
# Write only
attr_writer :internal_code
def initialize(name, price)
@name = name
@price = price
@created_at = Time.current
end
end
# Equivalent to:
class Product
def name
@name
end
def name=(value)
@name = value
end
def created_at
@created_at
end
def internal_code=(value)
@internal_code = value
end
end
Method Visibility
class BankAccount
def initialize(balance)
@balance = balance
end
# Public methods (default visibility)
def deposit(amount)
validate_amount(amount)
@balance += amount
end
# Protected methods (callable by instances of same class or subclasses)
protected
def compare_balance(other_account)
@balance <=> other_account.balance
end
def balance
@balance
end
# Private methods (only callable within instance)
private
def validate_amount(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
end
end
# Modern Ruby 3.x private syntax
class BankAccount
def deposit(amount)
validate_amount(amount) # OK - called on self
@balance += amount
end
private
# Can also use inline private
private def validate_amount(amount)
raise ArgumentError, "Amount must be positive" unless amount > 0
end
end
Self Keyword
class Document
attr_accessor :title
def initialize(title)
@title = title
end
# Instance method - self refers to instance
def display_title
self.title # same as @title
end
# Class method - self refers to class
def self.create_default
new("Untitled") # same as self.new("Untitled")
end
# Comparing self
def same_as?(other)
self == other
end
# Returning self for method chaining
def set_title(title)
@title = title
self # Return self for chaining
end
end
doc = Document.new("Report")
doc.set_title("Annual Report").display_title
Singleton Methods
# Define method on specific object instance
user = User.new("Alice", "alice@example.com")
def user.special_greeting
"Special greeting for #{@name}"
end
user.special_greeting # Works
other_user = User.new("Bob", "bob@example.com")
other_user.special_greeting # NoMethodError
Modules
Modules as Mixins
# Shared behavior across classes
module Timestampable
def mark_created
@created_at = Time.current
end
def mark_updated
@updated_at = Time.current
end
def timestamps
{ created_at: @created_at, updated_at: @updated_at }
end
end
class Article
include Timestampable
def initialize(title)
@title = title
mark_created
end
end
class Comment
include Timestampable
def initialize(body)
@body = body
mark_created
end
end
article = Article.new("Title")
article.timestamps
Modules as Namespaces
module Analytics
class Report
def generate
"Analytics Report"
end
end
class Dashboard
def display
"Analytics Dashboard"
end
end
end
report = Analytics::Report.new
dashboard = Analytics::Dashboard.new
Module Include vs Prepend vs Extend
module Greetable
def greet
"Hello from Greetable"
end
end
class Person
def greet
"Hello from Person"
end
end
# include - adds module methods as instance methods (after class methods in lookup chain)
class Person
include Greetable
end
Person.new.greet # => "Hello from Person" (class method takes precedence)
# prepend - adds module methods BEFORE class methods in lookup chain
class Person
prepend Greetable
end
Person.new.greet # => "Hello from Greetable" (module takes precedence)
# extend - adds module methods as CLASS methods
class Person
extend Greetable
end
Person.greet # => "Hello from Greetable" (class method)
Person.new.greet # NoMethodError
Method Lookup Chain
module A
def who_am_i
"A"
end
end
module B
def who_am_i
"B"
end
end
class C
prepend A
include B
def who_am_i
"C"
end
end
C.ancestors # => [A, C, B, Object, Kernel, BasicObject]
C.new.who_am_i # => "A" (prepended module wins)
Advanced Ruby OOP
Metaprogramming
define_method
class DynamicMethods
[:first_name, :last_name, :email].each do |attribute|
define_method(attribute) do
instance_variable_get("@#{attribute}")
end
define_method("#{attribute}=") do |value|
instance_variable_set("@#{attribute}", value)
end
end
end
user = DynamicMethods.new
user.first_name = "Alice"
user.first_name # => "Alice"
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
users = DynamicFinder.new([
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
])
users.find_by_name('Alice') # => { name: 'Alice', email: 'alice@example.com' }
users.find_by_email('bob@example.com') # => { name: 'Bob', email: 'bob@example.com' }
class_eval and instance_eval
# class_eval - evaluates code in context of class
User.class_eval do
def new_method
"defined dynamically"
end
end
# instance_eval - evaluates code in context of instance
user = User.new
user.instance_eval do
@secret = "secret value"
end
Eigenclass (Singleton Class)
class Person
def self.species
"Homo sapiens"
end
end
# Equivalent using eigenclass
class Person
class << self
def species
"Homo sapiens"
end
end
end
# Adding methods to eigenclass of instance
person = Person.new
class << person
def special_ability
"Can fly"
end
end
person.special_ability # Works
Person.new.special_ability # NoMethodError
Rails-Specific OOP
ActiveSupport::Concern
# Traditional module
module Taggable
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
has_many :tags, as: :taggable
end
end
module ClassMethods
def with_tag(tag_name)
joins(:tags).where(tags: { name: tag_name })
end
end
def tag_names
tags.pluck(:name)
end
end
# Using ActiveSupport::Concern (cleaner)
module Taggable
extend ActiveSupport::Concern
included do
has_many :tags, as: :taggable
end
class_methods do
def with_tag(tag_name)
joins(:tags).where(tags: { name: tag_name })
end
end
def tag_names
tags.pluck(:name)
end
end
# Usage
class Article < ApplicationRecord
include Taggable
end
Article.with_tag('ruby') # Class method
article.tag_names # Instance method
Service Objects
# Service object for complex business logic
class CreateOrderService
def initialize(user, cart)
@user = user
@cart = cart
end
def call
validate_cart!
ActiveRecord::Base.transaction do
@order = create_order
create_order_items
charge_payment
send_confirmation
clear_cart
end
Result.success(@order)
rescue => e
Result.failure(e.message)
end
private
def validate_cart!
raise "Cart is empty" if @cart.items.empty?
raise "Cart total invalid" unless @cart.total_valid?
end
def create_order
@user.orders.create!(total: @cart.total, status: 'pending')
end
def create_order_items
@cart.items.each do |cart_item|
@order.order_items.create!(
product: cart_item.product,
quantity: cart_item.quantity,
price: cart_item.price
)
end
end
def charge_payment
PaymentService.charge(@user, @cart.total)
end
def send_confirmation
OrderMailer.confirmation(@order).deliver_later
end
def clear_cart
@cart.clear!
end
end
# Result object
class Result
attr_reader :value, :error
def initialize(success, value, error = nil)
@success = success
@value = value
@error = error
end
def self.success(value)
new(true, value)
end
def self.failure(error)
new(false, nil, error)
end
def success?
@success
end
def failure?
!@success
end
end
# Usage
result = CreateOrderService.new(user, cart).call
if result.success?
redirect_to order_path(result.value)
else
flash[:error] = result.error
redirect_to cart_path
end
Form Objects
class UserRegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email, :string
attribute :password, :string
attribute :password_confirmation, :string
attribute :first_name, :string
attribute :last_name, :string
attribute :accept_terms, :boolean
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, presence: true, length: { minimum: 8 }
validates :password_confirmation, presence: true
validates :first_name, :last_name, presence: true
validates :accept_terms, acceptance: true
validate :passwords_match
def save
return false unless valid?
ActiveRecord::Base.transaction do
@user = User.create!(
email: email,
password: password,
first_name: first_name,
last_name: last_name
)
@profile = @user.create_profile!
send_welcome_email
end
true
rescue => e
errors.add(:base, e.message)
false
end
attr_reader :user
private
def passwords_match
if password != password_confirmation
errors.add(:password_confirmation, "doesn't match password")
end
end
def send_welcome_email
UserMailer.welcome(@user).deliver_later
end
end
# Usage in controller
def create
@form = UserRegistrationForm.new(registration_params)
if @form.save
redirect_to dashboard_path
else
render :new
end
end
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(last_login_at: :desc)
end
# Chainable query methods
def with_subscription
@relation = @relation.joins(:subscription).where.not(subscriptions: { expires_at: nil })
self
end
def by_role(role)
@relation = @relation.where(role: role)
self
end
end
# Usage
ActiveUsersQuery.new.call
ActiveUsersQuery.new.with_subscription.call
ActiveUsersQuery.new.by_role('admin').with_subscription.call
Policy Objects (Pundit)
# app/policies/post_policy.rb
class PostPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def index?
true
end
def show?
post.published? || post.author == user || user&.admin?
end
def create?
user.present?
end
def update?
post.author == user || user&.admin?
end
def destroy?
post.author == user || user&.admin?
end
def publish?
(post.author == user && post.draft?) || user&.admin?
end
class Scope
attr_reader :user, :scope
def initialize(user, scope)
@user = user
@scope = scope
end
def resolve
if user&.admin?
scope.all
elsif user
scope.where(published: true).or(scope.where(author: user))
else
scope.where(published: true)
end
end
end
end
# Usage
authorize @post # Calls PostPolicy#show?
@posts = policy_scope(Post) # Calls PostPolicy::Scope#resolve
Presenter/Decorator Objects
class UserPresenter
def initialize(user, view_context)
@user = user
@view = view_context
end
def full_name
[@user.first_name, @user.last_name].join(' ')
end
def avatar_url
@user.avatar.attached? ? @view.url_for(@user.avatar) : @view.asset_path('default_avatar.png')
end
def formatted_created_at
@view.time_ago_in_words(@user.created_at) + " ago"
end
def profile_link
@view.link_to(full_name, @view.user_path(@user))
end
# Delegate unknown methods to user
def method_missing(method, *args, &block)
@user.send(method, *args, &block)
end
def respond_to_missing?(method, include_private = false)
@user.respond_to?(method, include_private) || super
end
end
# Using Draper gem (recommended)
class UserDecorator < Draper::Decorator
delegate_all
def full_name
[object.first_name, object.last_name].join(' ')
end
def avatar_url
object.avatar.attached? ? h.url_for(object.avatar) : h.asset_path('default_avatar.png')
end
def formatted_created_at
h.time_ago_in_words(object.created_at) + " ago"
end
end
# Usage
@user = User.find(params[:id]).decorate
@user.full_name
@user.formatted_created_at
Value Objects
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
@amount = BigDecimal(amount.to_s)
@currency = currency
end
def +(other)
raise ArgumentError, "Currency mismatch" unless currency == other.currency
Money.new(amount + other.amount, currency)
end
def -(other)
raise ArgumentError, "Currency mismatch" unless currency == other.currency
Money.new(amount - other.amount, currency)
end
def *(multiplier)
Money.new(amount * multiplier, currency)
end
def <=>(other)
return nil unless currency == other.currency
amount <=> other.amount
end
def ==(other)
other.is_a?(Money) && amount == other.amount && currency == other.currency
end
alias eql? ==
def hash
[amount, currency].hash
end
def to_s
format("%.2f %s", amount, currency)
end
end
# Usage
price = Money.new(29.99, 'USD')
tax = Money.new(2.10, 'USD')
total = price + tax # => Money object
total.to_s # => "32.09 USD"
SOLID Principles in Ruby
Single Responsibility Principle (SRP)
Each class should have one reason to change.
# BAD - Multiple responsibilities
class User
def create
# Database logic
ActiveRecord::Base.connection.execute("INSERT INTO users...")
# Email logic
send_welcome_email
# Analytics logic
track_signup_event
end
end
# GOOD - Separated responsibilities
class User < ApplicationRecord
after_create :send_welcome_email
after_create :track_signup
private
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
def track_signup
AnalyticsService.track_event(self, 'signup')
end
end
class UserMailer < ApplicationMailer
def welcome(user)
# Email logic
end
end
class AnalyticsService
def self.track_event(user, event_name)
# Analytics logic
end
end
Open/Closed Principle (OCP)
Open for extension, closed for modification.
# BAD - Must modify class to add payment methods
class PaymentProcessor
def process(payment_method, amount)
case payment_method
when 'credit_card'
process_credit_card(amount)
when 'paypal'
process_paypal(amount)
when 'bitcoin'
process_bitcoin(amount)
end
end
end
# GOOD - Open for extension via polymorphism
class PaymentProcessor
def process(payment_method, amount)
payment_method.process(amount)
end
end
class CreditCardPayment
def process(amount)
# Credit card processing
end
end
class PaypalPayment
def process(amount)
# PayPal processing
end
end
class BitcoinPayment
def process(amount)
# Bitcoin processing
end
end
# Usage
processor = PaymentProcessor.new
processor.process(CreditCardPayment.new, 100)
processor.process(PaypalPayment.new, 100)
Liskov Substitution Principle (LSP)
Subclasses should be substitutable for their base classes.
# BAD - Breaks LSP
class Rectangle
attr_accessor :width, :height
def area
width * height
end
end
class Square < Rectangle
def width=(value)
super
@height = value
end
def height=(value)
super
@width = value
end
end
# This breaks expectations:
rect = Square.new
rect.width = 5
rect.height = 10
rect.area # => 100 (expected 50)
# GOOD - Composition over inheritance
class Rectangle
attr_accessor :width, :height
def area
width * height
end
end
class Square
attr_reader :side
def initialize(side)
@side = side
end
def area
side * side
end
end
Interface Segregation Principle (ISP)
Clients shouldn't depend on interfaces they don't use.
# BAD - Fat interface
module Worker
def work
raise NotImplementedError
end
def eat
raise NotImplementedError
end
def sleep
raise NotImplementedError
end
end
class Human
include Worker
def work; end
def eat; end
def sleep; end
end
class Robot
include Worker
def work; end
def eat; raise "Robots don't eat"; end
def sleep; raise "Robots don't sleep"; end
end
# GOOD - Segregated interfaces
module Workable
def work
raise NotImplementedError
end
end
module Eatable
def eat
raise NotImplementedError
end
end
module Sleepable
def sleep
raise NotImplementedError
end
end
class Human
include Workable
include Eatable
include Sleepable
def work; end
def eat; end
def sleep; end
end
class Robot
include Workable
def work; end
end
Dependency Inversion Principle (DIP)
Depend on abstractions, not concretions.
# BAD - Direct dependency on concrete class
class OrderProcessor
def process(order)
notifier = EmailNotifier.new
notifier.send(order.customer.email, "Order processed")
end
end
# GOOD - Dependency injection
class OrderProcessor
def initialize(notifier)
@notifier = notifier
end
def process(order)
@notifier.send(order.customer.email, "Order processed")
end
end
class EmailNotifier
def send(email, message)
# Send email
end
end
class SmsNotifier
def send(phone, message)
# Send SMS
end
end
# Usage
email_processor = OrderProcessor.new(EmailNotifier.new)
sms_processor = OrderProcessor.new(SmsNotifier.new)
Inheritance Patterns
Single Table Inheritance (STI)
# app/models/vehicle.rb
class Vehicle < ApplicationRecord
# Table: vehicles
# Columns: id, type, name, max_speed, max_altitude, max_depth
end
class Car < Vehicle
def drive
"Driving at #{max_speed} mph"
end
end
class Airplane < Vehicle
def fly
"Flying at #{max_altitude} ft"
end
end
class Submarine < Vehicle
def dive
"Diving to #{max_depth} ft"
end
end
# Usage
car = Car.create(name: 'Tesla', max_speed: 150)
airplane = Airplane.create(name: 'Boeing 747', max_altitude: 45000)
Vehicle.all # Returns mix of Cars, Airplanes, Submarines
Car.all # Returns only Cars
When to use STI:
- Subclasses share most attributes
- Subclasses have similar behavior
- Need to query across all types
STI Anti-patterns:
- Too many type-specific columns (use delegated_type instead)
- Vastly different behavior between types
- Deep inheritance hierarchies
Delegated Type (Rails 6.1+)
# Better than STI when types have different attributes
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[Message Comment]
end
class Message < ApplicationRecord
has_one :entry, as: :entryable, touch: true
end
class Comment < ApplicationRecord
has_one :entry, as: :entryable, touch: true
end
# Usage
entry = Entry.find(1)
entry.entryable # Returns Message or Comment
entry.message? # true if type is Message
entry.comment? # true if type is Comment
Polymorphic Associations
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Article < ApplicationRecord
has_many :comments, as: :commentable
end
class Video < ApplicationRecord
has_many :comments, as: :commentable
end
# Usage
article = Article.find(1)
article.comments.create(body: "Great article!")
video = Video.find(1)
video.comments.create(body: "Great video!")
Composition Over Inheritance
Delegation with Forwardable
require 'forwardable'
class Order
extend Forwardable
def_delegators :@customer, :name, :email, :phone
def_delegators :@line_items, :<<, :each, :size
def initialize(customer)
@customer = customer
@line_items = []
end
end
order = Order.new(customer)
order.name # Delegates to customer.name
order << line_item # Delegates to line_items.<<
SimpleDelegator
require 'delegate'
class UserDecorator < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
def greeting
"Hello, #{full_name}!"
end
end
user = User.new(first_name: 'Alice', last_name: 'Smith')
decorated = UserDecorator.new(user)
decorated.full_name # => "Alice Smith"
decorated.email # Delegates to user.email
ActiveSupport::Delegation
class Order
delegate :name, :email, to: :customer, prefix: true
delegate :total_price, to: :calculator
def initialize(customer, items)
@customer = customer
@items = items
end
private
def calculator
@calculator ||= PriceCalculator.new(@items)
end
end
order.customer_name # => customer.name
order.customer_email # => customer.email
order.total_price # => calculator.total_price
Modern Ruby Features
Pattern Matching (Ruby 3.x)
def process_response(response)
case response
in { status: 200, body: }
puts "Success: #{body}"
in { status: 404 }
puts "Not found"
in { status: 500..599, error: }
puts "Server error: #{error}"
else
puts "Unknown response"
end
end
# Array pattern matching
def process_coordinates(point)
case point
in [0, 0]
"Origin"
in [x, 0]
"On X axis at #{x}"
in [0, y]
"On Y axis at #{y}"
in [x, y]
"Point at (#{x}, #{y})"
end
end
Endless Methods (Ruby 3.x)
# Traditional
def full_name
"#{first_name} #{last_name}"
end
# Endless method
def full_name = "#{first_name} #{last_name}"
# With arguments
def greet(name) = "Hello, #{name}!"
# Useful for simple one-liners
def adult? = age >= 18
def total = items.sum(&:price)
Keyword Arguments
# Required keyword arguments
def create_user(email:, password:, role: 'member')
User.create(email: email, password: password, role: role)
end
create_user(email: 'user@example.com', password: 'secret')
# **kwargs for flexibility
def build_query(**options)
options.each do |key, value|
puts "#{key}: #{value}"
end
end
build_query(name: 'Alice', age: 30, city: 'NYC')
Numbered Parameters
# Traditional
users.map { |user| user.name.upcase }
# Numbered parameters (Ruby 2.7+)
users.map { _1.name.upcase }
# Multiple parameters
users.zip(posts).map { "#{_1.name}: #{_2.title}" }
Testing OOP Code
Testing Private Methods
# Generally, don't test private methods directly
# Test through public interface instead
class Calculator
def calculate(numbers)
validate_input(numbers)
sum(numbers)
end
private
def validate_input(numbers)
raise ArgumentError unless numbers.is_a?(Array)
end
def sum(numbers)
numbers.sum
end
end
# Test public method (which exercises private methods)
RSpec.describe Calculator do
describe '#calculate' do
it 'calculates sum' do
expect(subject.calculate([1, 2, 3])).to eq(6)
end
it 'validates input' do
expect { subject.calculate("invalid") }.to raise_error(ArgumentError)
end
end
end
# If you MUST test private method
RSpec.describe Calculator do
describe '#sum' do
it 'sums numbers' do
expect(subject.send(:sum, [1, 2, 3])).to eq(6)
end
end
end
Testing Modules
module Taggable
def add_tag(tag)
@tags ||= []
@tags << tag
end
def tags
@tags || []
end
end
# Create dummy class for testing
RSpec.describe Taggable do
let(:dummy_class) { Class.new { include Taggable } }
let(:instance) { dummy_class.new }
describe '#add_tag' do
it 'adds tag' do
instance.add_tag('ruby')
expect(instance.tags).to include('ruby')
end
end
end
Mocking and Stubbing
RSpec.describe OrderProcessor do
describe '#process' do
let(:order) { double('Order', id: 1, total: 100) }
let(:payment_gateway) { double('PaymentGateway') }
subject { OrderProcessor.new(payment_gateway) }
it 'charges payment' do
expect(payment_gateway).to receive(:charge).with(100)
subject.process(order)
end
it 'handles payment failure' do
allow(payment_gateway).to receive(:charge).and_raise(PaymentError)
expect { subject.process(order) }.to raise_error(PaymentError)
end
end
end
Rails Anti-Patterns
Fat Models
# BAD - Fat model with too many responsibilities
class User < ApplicationRecord
def create_with_profile(params)
# Creation logic
end
def send_welcome_email
# Email logic
end
def calculate_statistics
# Analytics logic
end
def export_to_csv
# Export logic
end
end
# GOOD - Thin model with extracted services
class User < ApplicationRecord
# Only model-specific logic and validations
validates :email, presence: true, uniqueness: true
has_many :posts
end
class UserCreationService
def call(params)
# Creation logic
end
end
class UserStatisticsService
def call(user)
# Analytics logic
end
end
class UserCsvExporter
def export(users)
# Export logic
end
end
Callback Hell
# BAD - Too many callbacks
class Order < ApplicationRecord
before_validation :set_defaults
after_validation :check_inventory
before_create :generate_order_number
after_create :send_confirmation
after_create :update_inventory
after_create :create_invoice
after_create :notify_warehouse
after_update :log_changes
before_destroy :cancel_pending_shipments
end
# GOOD - Explicit service object
class CreateOrderService
def call(params)
order = Order.new(params)
order.set_defaults
return Result.failure(order.errors) unless order.valid?
ActiveRecord::Base.transaction do
order.generate_order_number
order.save!
send_confirmation(order)
update_inventory(order)
create_invoice(order)
notify_warehouse(order)
end
Result.success(order)
end
end
God Objects
# BAD - One object does everything
class ApplicationController < ActionController::Base
def current_user
# User logic
end
def authorize_admin
# Authorization logic
end
def log_action(action)
# Logging logic
end
def send_notification(message)
# Notification logic
end
def format_date(date)
# Formatting logic
end
end
# GOOD - Separated concerns
class ApplicationController < ActionController::Base
include Authentication
include Authorization
include Logging
end
module Authentication
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
end
module Authorization
def authorize_admin
redirect_to root_path unless current_user&.admin?
end
end
Summary Checklist
Good OOP Practices:
- Use meaningful class and method names
- Keep classes focused (Single Responsibility Principle)
- Prefer composition over inheritance
- Use modules for shared behavior
- Make dependencies explicit (dependency injection)
- Write tests for public interfaces
- Use service objects for complex business logic
- Use form objects for complex forms
- Use query objects for complex queries
- Use presenters/decorators for view logic
- Follow SOLID principles
- Avoid callback hell - use service objects
- Avoid fat models - extract to services
- Use value objects for domain concepts
This skill provides comprehensive OOP patterns for Ruby and Rails development!