Design Patterns in Ruby – Simplified with Real-World Scenarios (By Russ Olsen)

Design Patterns in Ruby – Simplified with Real-World Scenarios (By Russ Olsen)

🔰 Introduction

Design patterns are time-tested solutions to common problems in software design. This book simplifies classic patterns using Ruby’s expressive features like blocks, mixins, metaprogramming, and open classes. The goal is not just to learn patterns, but to understand how to apply them elegantly.

Legacy Ruby Docker Image

🏗️ Creational Patterns – Object Creation Made Flexible

🔹 Singleton Pattern

Purpose: Ensure only one instance of a class exists.

Real-World Scenario: App configuration – we only need one configuration manager across the entire application.

Ruby Code:

require 'singleton'

class AppConfig
  include Singleton
  attr_accessor :settings
end

AppConfig.instance.settings = { theme: 'dark', locale: 'en' }

How it helps: You always access AppConfig.instance without worrying about duplicate instances or shared state.

🔹 Factory Method

Purpose: Delegates object creation to subclasses or decision logic.

Use Case: You have different types of notifications (email, SMS, push). Let the factory decide which to instantiate.

Code Example:

class NotificationFactory
  def self.build(type)
    case type
    when :email then EmailNotification.new
    when :sms then SMSNotification.new
    when :push then PushNotification.new
    else raise 'Unknown type'
    end
  end
end

notif = NotificationFactory.build(:email)
notif.send("Hello")

How it helps: Decouples creation logic and centralizes it, making the code easier to scale and test.


🧱 Structural Patterns – Organizing Code for Flexibility

🔹 Adapter Pattern

Purpose: Allows incompatible interfaces to work together.

Real-World Use Case: You’re integrating a third-party payment gateway, but its method names don’t match your system.

Code:

class LegacyPayment
  def execute_payment(amount)
    puts "Paid #{amount} via legacy gateway"
  end
end

class PaymentAdapter
  def initialize(service)
    @service = service
  end

  def pay(amount)
    @service.execute_payment(amount)
  end
end

payment = PaymentAdapter.new(LegacyPayment.new)
payment.pay(100)

How it helps: Adapts the legacy system to your modern interface, avoiding rewriting or deep refactoring.

🔹 Decorator Pattern

Purpose: Dynamically adds behavior to objects.

Use Case: You want to log or monitor service usage without changing the original class.

Code:

class BasicNotifier
  def send(message)
    puts "Sending: #{message}"
  end
end

class LoggingNotifier
  def initialize(notifier)
    @notifier = notifier
  end

  def send(message)
    puts "[LOG] Sending message"
    @notifier.send(message)
  end
end

notifier = LoggingNotifier.new(BasicNotifier.new)
notifier.send("Hello World")

How it helps: Allows layering of responsibilities (like logging, caching, authentication) without altering core logic.


🔁 Behavioral Patterns – Managing Communication & Flow

🔹 Observer Pattern

Purpose: One object notifies others when its state changes.

Real Use Case: A blog CMS that notifies subscribers when a new article is published.

Code:

require 'observer'

class Blog
  include Observable

  def publish(title)
    puts "Publishing: #{title}"
    changed
    notify_observers(title)
  end
end

class Subscriber
  def update(title)
    puts "New article notification: #{title}"
  end
end

blog = Blog.new
blog.add_observer(Subscriber.new)
blog.publish("Design Patterns in Ruby")

How it helps: Decouples publisher and subscriber logic, enhancing flexibility and reusability.

🔹 Strategy Pattern

Purpose: Encapsulates interchangeable algorithms/behaviors.

Use Case: Different sorting strategies in an e-commerce site (e.g., price, rating, popularity).

Code:

class Sorter
  attr_writer :strategy

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

class PriceSort
  def self.sort(items)
    items.sort_by { |item| item[:price] }
  end
end

class RatingSort
  def self.sort(items)
    items.sort_by { |item| -item[:rating] }
  end
end

items = [{ price: 100, rating: 4.5 }, { price: 50, rating: 4.8 }]
sorter = Sorter.new
sorter.strategy = PriceSort
p sorter.sort(items)

How it helps: Promotes clean separation of algorithms and makes your system extensible.


🧙 Metaprogramming and Patterns

Metaprogramming allows you to build powerful, reusable solutions like:

Example: Create dynamic methods:

class Report
  [:pdf, :csv, :html].each do |format|
    define_method("generate_#{format}") do
      puts "Generating #{format} report..."
    end
  end
end

r = Report.new
r.generate_pdf

Why it matters: Reduces repetition, increases flexibility.


✅ Best Practices


🎯 Final Thoughts

Design patterns are not rigid templates. They are tools in your toolbox to write better Ruby code.

Want to go deeper? Re-implement these patterns using your own project. That’s how you make them stick.