--- id: geoffjay/claude-plugins/ruby-patterns version: "4e79844b" license: MIT install: manual updated: 2025-11-04 --- # ruby-patterns — Ruby Patterns equips you with creational, structural, and behavioral design patterns alongside modern Ruby idioms and metaprogramming techniques. Reference common shortcuts for conditionals, enumerables, and strings, then dive into factory, builder, decorator, strategy, and observer patterns with working examples. Use it to refactor for clarity and apply proven architectural approaches. Publisher: geoffjay · Stars: 8 · Updated: 2025-11-04 Install (manual): `git clone https://github.com/geoffjay/claude-plugins` ## SKILL.md # 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 [View on SkillFed](https://skillfed.io/geoffjay/claude-plugins/ruby-patterns) · [View on GitHub](https://github.com/geoffjay/claude-plugins)