Files
2025-11-29 18:28:07 +08:00

14 KiB

name, description
name description
ruby-patterns 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 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