855 lines
14 KiB
Markdown
855 lines
14 KiB
Markdown
---
|
|
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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
# Interpolation
|
|
"Hello #{name}!"
|
|
|
|
# Safe interpolation
|
|
"Result: %{value}" % { value: result }
|
|
|
|
# Multiline
|
|
<<~TEXT
|
|
Heredoc with indentation
|
|
removed automatically
|
|
TEXT
|
|
```
|
|
|
|
### Block Syntax
|
|
|
|
```ruby
|
|
# 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
|
|
|
|
```ruby
|
|
def process(user)
|
|
return unless user
|
|
return unless user.active?
|
|
|
|
# Main logic here
|
|
end
|
|
```
|
|
|
|
### Case Statements
|
|
|
|
```ruby
|
|
# 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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
# 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:**
|
|
```ruby
|
|
# 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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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):**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
# 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:**
|
|
```ruby
|
|
# 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:**
|
|
```ruby
|
|
# 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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
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:**
|
|
```ruby
|
|
# 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:**
|
|
```ruby
|
|
# 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 idioms
|
|
- `design-patterns.rb` - Complete implementations of all patterns
|
|
- `metaprogramming-examples.rb` - Advanced metaprogramming techniques
|
|
|
|
See `references/` directory for:
|
|
- Style guides and best practices
|
|
- Performance optimization examples
|
|
- Testing pattern library
|