design-patterns

Installation
SKILL.md

Design Patterns in Ruby

Idiomatic Ruby implementations of common design patterns.

SOLID Principles in Ruby

Single Responsibility Principle

# Bad: User handles too many concerns
class User
  def save
    validate!
    Database.insert(self)
    Mailer.send_welcome_email(self)
    Analytics.track("user_created", self)
  end
end

# Good: Each class has one reason to change
class User
  def save
    validate!
    UserRepository.save(self)
  end
end

class UserRegistrationService
  def initialize(user)
    @user = user
  end

  def call
    @user.save
    WelcomeMailer.deliver(@user)
    Analytics.track("user_created", @user)
  end
end

Open/Closed Principle

# Open for extension, closed for modification
class PaymentProcessor
  def initialize(strategy)
    @strategy = strategy
  end

  def process(amount)
    @strategy.charge(amount)
  end
end

class StripePayment
  def charge(amount)
    Stripe::Charge.create(amount: amount)
  end
end

class PaypalPayment
  def charge(amount)
    Paypal::Payment.execute(amount)
  end
end

# Add new payment methods without modifying PaymentProcessor
class CryptoPayment
  def charge(amount)
    Crypto::Transaction.send(amount)
  end
end

Liskov Substitution Principle

# Subtypes must be substitutable for their base types
class Bird
  def fly
    raise NotImplementedError
  end
end

# Bad: Penguin can't fly, violates LSP
class Penguin < Bird
  def fly
    raise "Penguins can't fly!"
  end
end

# Good: Separate flying capability
module Flyable
  def fly
    raise NotImplementedError
  end
end

class Bird; end

class Sparrow < Bird
  include Flyable

  def fly
    "Flying high!"
  end
end

class Penguin < Bird
  def swim
    "Swimming fast!"
  end
end

Interface Segregation Principle

# Prefer small, focused interfaces
# Bad: One big interface
module Worker
  def work; end
  def eat; end
  def sleep; end
end

# Good: Separate concerns
module Workable
  def work
    raise NotImplementedError
  end
end

module Feedable
  def eat
    raise NotImplementedError
  end
end

class Human
  include Workable
  include Feedable

  def work = "Working..."
  def eat = "Eating..."
end

class Robot
  include Workable

  def work = "Processing..."
  # Robots don't need to eat
end

Dependency Inversion Principle

# Depend on abstractions, not concretions
# Bad: High-level module depends on low-level module
class Report
  def initialize
    @formatter = HTMLFormatter.new
  end

  def generate(data)
    @formatter.format(data)
  end
end

# Good: Depend on abstraction (duck typing in Ruby)
class Report
  def initialize(formatter)
    @formatter = formatter
  end

  def generate(data)
    @formatter.format(data)
  end
end

class HTMLFormatter
  def format(data) = "<html>#{data}</html>"
end

class JSONFormatter
  def format(data) = data.to_json
end

Report.new(HTMLFormatter.new).generate(data)
Report.new(JSONFormatter.new).generate(data)

Creational Patterns

Factory Method

class DocumentFactory
  def self.create(type, **options)
    case type
    when :pdf then PDFDocument.new(**options)
    when :word then WordDocument.new(**options)
    when :html then HTMLDocument.new(**options)
    else raise ArgumentError, "Unknown document type: #{type}"
    end
  end
end

document = DocumentFactory.create(:pdf, title: "Report")

Abstract Factory

class UIFactory
  def create_button
    raise NotImplementedError
  end

  def create_input
    raise NotImplementedError
  end
end

class DarkThemeFactory < UIFactory
  def create_button = DarkButton.new
  def create_input = DarkInput.new
end

class LightThemeFactory < UIFactory
  def create_button = LightButton.new
  def create_input = LightInput.new
end

def build_form(factory)
  button = factory.create_button
  input = factory.create_input
  Form.new(button, input)
end

Builder

class QueryBuilder
  def initialize
    @select = "*"
    @from = nil
    @where = []
    @order = nil
    @limit = nil
  end

  def select(*columns)
    @select = columns.join(", ")
    self
  end

  def from(table)
    @from = table
    self
  end

  def where(condition)
    @where << condition
    self
  end

  def order(column, direction = :asc)
    @order = "#{column} #{direction.upcase}"
    self
  end

  def limit(n)
    @limit = n
    self
  end

  def to_sql
    sql = "SELECT #{@select} FROM #{@from}"
    sql += " WHERE #{@where.join(' AND ')}" if @where.any?
    sql += " ORDER BY #{@order}" if @order
    sql += " LIMIT #{@limit}" if @limit
    sql
  end
end

query = QueryBuilder.new
  .select(:id, :name, :email)
  .from(:users)
  .where("active = true")
  .where("created_at > '2024-01-01'")
  .order(:created_at, :desc)
  .limit(10)
  .to_sql

Singleton (Using Module)

# Ruby-idiomatic singleton using module
module Configuration
  class << self
    attr_accessor :api_key, :environment

    def configure
      yield self
    end

    def production?
      environment == :production
    end
  end
end

Configuration.configure do |config|
  config.api_key = "secret"
  config.environment = :production
end

Configuration.api_key  # => "secret"

Structural Patterns

Decorator (Using Modules)

class Coffee
  def cost = 2.0
  def description = "Coffee"
end

module Milk
  def cost = super + 0.5
  def description = "#{super} with milk"
end

module Sugar
  def cost = super + 0.25
  def description = "#{super} with sugar"
end

module Whip
  def cost = super + 0.75
  def description = "#{super} with whip"
end

coffee = Coffee.new
coffee.extend(Milk)
coffee.extend(Sugar)
coffee.extend(Whip)

coffee.description  # => "Coffee with milk with sugar with whip"
coffee.cost         # => 3.5

Adapter

# Existing interface
class OldPrinter
  def print_document(text)
    puts "Printing: #{text}"
  end
end

# New interface we want to use
class ModernPrinter
  def render(document)
    puts "Rendering: #{document.content}"
  end
end

# Adapter
class PrinterAdapter
  def initialize(modern_printer)
    @printer = modern_printer
  end

  def print_document(text)
    document = OpenStruct.new(content: text)
    @printer.render(document)
  end
end

# Usage
printer = PrinterAdapter.new(ModernPrinter.new)
printer.print_document("Hello")  # Works with old interface

Facade

class OrderFacade
  def initialize(user)
    @user = user
    @cart = ShoppingCart.new(user)
    @inventory = InventoryService.new
    @payment = PaymentService.new
    @shipping = ShippingService.new
  end

  def place_order(payment_details)
    items = @cart.items

    # Complex subsystem interactions hidden
    @inventory.reserve(items)
    @payment.charge(@user, @cart.total, payment_details)
    order = Order.create(user: @user, items: items)
    @shipping.schedule(order)
    @cart.clear

    order
  rescue PaymentError => e
    @inventory.release(items)
    raise
  end
end

# Simple interface for clients
facade = OrderFacade.new(current_user)
order = facade.place_order(credit_card_info)

Behavioral Patterns

Strategy (Using Blocks/Procs)

class Sorter
  def initialize(&strategy)
    @strategy = strategy || ->(a, b) { a <=> b }
  end

  def sort(items)
    items.sort(&@strategy)
  end
end

# Different strategies
by_name = Sorter.new { |a, b| a.name <=> b.name }
by_price = Sorter.new { |a, b| a.price <=> b.price }
by_date_desc = Sorter.new { |a, b| b.date <=> a.date }

by_name.sort(products)
by_price.sort(products)

Observer

module Observable
  def add_observer(observer)
    observers << observer
  end

  def remove_observer(observer)
    observers.delete(observer)
  end

  def notify_observers(event, data = nil)
    observers.each { |o| o.update(event, data) }
  end

  private

  def observers
    @observers ||= []
  end
end

class Order
  include Observable

  attr_reader :status

  def complete!
    @status = :completed
    notify_observers(:order_completed, self)
  end
end

class EmailNotifier
  def update(event, order)
    case event
    when :order_completed
      send_confirmation_email(order)
    end
  end
end

class InventoryTracker
  def update(event, order)
    case event
    when :order_completed
      reduce_stock(order.items)
    end
  end
end

order = Order.new
order.add_observer(EmailNotifier.new)
order.add_observer(InventoryTracker.new)
order.complete!

Command

class Command
  def execute
    raise NotImplementedError
  end

  def undo
    raise NotImplementedError
  end
end

class AddItemCommand < Command
  def initialize(cart, item)
    @cart = cart
    @item = item
  end

  def execute
    @cart.add(@item)
  end

  def undo
    @cart.remove(@item)
  end
end

class CommandHistory
  def initialize
    @history = []
  end

  def execute(command)
    command.execute
    @history.push(command)
  end

  def undo
    command = @history.pop
    command&.undo
  end
end

history = CommandHistory.new
history.execute(AddItemCommand.new(cart, item1))
history.execute(AddItemCommand.new(cart, item2))
history.undo  # Removes item2

Additional Resources

Reference Files

  • references/pattern-examples.md - Extended examples and anti-patterns
Weekly Installs
2
GitHub Stars
2
First Seen
Apr 15, 2026