Initial commit
This commit is contained in:
854
skills/ruby-patterns/SKILL.md
Normal file
854
skills/ruby-patterns/SKILL.md
Normal file
@@ -0,0 +1,854 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user