In the world of software development, design patterns and anti-patterns play a critical role in writing maintainable, clean, and scalable code. As a Ruby developer, mastering these concepts will help you design robust applications and avoid common pitfalls.
What are Design Patterns?
Design patterns are time-tested, reusable solutions to common problems in software design. They aren’t code snippets but conceptual templates you can adapt based on your needs. Think of them as architectural blueprints.
๐งฑ Common Ruby Design Patterns
1. Singleton Pattern
the_logger = Logger.new
class Logger
@@instance = Logger.new
def self.instance
@@instance
end
private_class_method :new
end
Use when only one instance of a class should exist (e.g., logger, configuration manager).
๐ง What is the Singleton Pattern?
The Singleton Pattern ensures that only one instance of a class exists during the lifetime of an application. This is useful for managing shared resources like:
- a logger (so logs are not duplicated or misdirected),
- a configuration manager (so all parts of the app read/write the same config),
- or a database connection pool.
๐ Why private_class_method :new?
This line prevents other parts of the code from calling Logger.new, like this:
Logger.new # โ Will raise a NoMethodError
So, you’re restricting object creation from outside the class.
Q) Then how do you get an object of Logger?
By using a class method like Logger.instance that returns the only instance of the class.
โ
Full Singleton Example in Ruby
class Logger
# Create and store the single instance
@@instance = Logger.new
# Provide a public way to access that instance
def self.instance
@@instance
end
# Prevent external instantiation
private_class_method :new
# Example method
def log(message)
puts "[LOG] #{message}"
end
end
# Usage
logger1 = Logger.instance
logger2 = Logger.instance
logger1.log("Singleton works!")
puts logger1.object_id == logger2.object_id # true
๐งพ Explanation:
@@instance = Logger.new: Creates the only instance when the class is loaded.
Logger.instance: The only way to access that object.
private_class_method :new: Prevents creation of new objects using Logger.new.
logger1 == logger2: โ
True, because they point to the same object.
๐ฌ Think of a Real-World Example
Imagine a central control tower at an airport:
- There should only be one control tower instance managing flights.
- If each plane connected to a new tower, it would be chaos!
The Singleton pattern in Ruby ensures there’s just one control tower (object) shared globally.
2. Observer Pattern
class Order
include Observable
def place_order
changed
notify_observers(self)
end
end
class EmailNotifier
def update(order)
puts "Email sent for order #{order.id}"
end
end
order = Order.new
order.add_observer(EmailNotifier.new)
order.place_order
Use when one change in an object should trigger actions in other objects.
๐ Observer Pattern in Ruby โ Explained in Detail
The Observer Pattern is a behavioral design pattern that lets one object (the subject) notify other objects (the observers) when its state changes.
Ruby has built-in support for this through the Observable module in the standard library (require 'observer').
How it Works
- The Subject (e.g.,
Order) includes the Observable module.
- The subject calls:
changed โ marks the object as changed.
notify_observers(data) โ notifies all subscribed observers.
- Observers (e.g.,
EmailNotifier) implement an update method.
- Observers are registered using
add_observer(observer_object).
๐งช Complete Working Example
require 'observer'
class Order
include Observable
attr_reader :id
def initialize(id)
@id = id
end
def place_order
puts "Placing order #{@id}..."
changed # Mark this object as changed
notify_observers(self) # Notify all observers
end
end
class EmailNotifier
def update(order)
puts "๐ง Email sent for Order ##{order.id}"
end
end
class SMSNotifier
def update(order)
puts "๐ฑ SMS sent for Order ##{order.id}"
end
end
# Create subject and observers
order = Order.new(101)
order.add_observer(EmailNotifier.new)
order.add_observer(SMSNotifier.new)
order.place_order
# Output:
# Placing order 101...
# ๐ง Email sent for Order #101
# ๐ฑ SMS sent for Order #101
๐ง When to Use the Observer Pattern
- You have one object whose changes should automatically update other dependent objects.
- Examples:
- UI updates in response to data changes
- Logging, email, or analytics triggers after a user action
- Notification systems in event-driven apps
Updated Observer Pattern
require 'observer'
class Order
include Observable
attr_reader :id
def initialize(id)
@id = id
end
def place_order
changed
notify_observers(self)
end
end
class EmailNotifier
def update(order)
puts "๐ง Email sent for Order ##{order.id}"
end
end
order = Order.new(42)
order.add_observer(EmailNotifier.new)
order.place_order
What’s Happening?
Order includes the Observable module to gain observer capabilities.
add_observer registers an observer object.
- When
place_order is called:
changed marks the state as changed.
notify_observers(self) triggers the observer’s update method.
EmailNotifier reacts to the change โ in this case, it simulates sending an email.
๐งฉ Use this pattern when one change should trigger multiple actions โ like sending notifications, logging, or syncing data across objects.
Check: https://docs.ruby-lang.org/en/2.2.0/Observable.html
3. Decorator Pattern
class SimpleCoffee
def cost
2
end
end
class MilkDecorator
def initialize(coffee)
@coffee = coffee
end
def cost
@coffee.cost + 0.5
end
end
coffee = MilkDecorator.new(SimpleCoffee.new)
puts coffee.cost # => 2.5
Add responsibilities to objects dynamically without modifying their code.
๐ 4. Strategy Pattern
The Strategy Pattern allows choosing an algorithm’s behaviour at runtime. This pattern is useful when you have multiple interchangeable ways to perform a task.
โ
Example: Text Formatter Strategies
Let’s say you have a system that outputs text in different formats โ plain, HTML, or Markdown.
โ Before Using Strategy Pattern โ Hardcoded Conditional Formatting
class TextFormatter
def initialize(format)
@format = format
end
def format(text)
case @format
when :plain
text
when :html
"<p>#{text}</p>"
when :markdown
"**#{text}**"
else
raise "Unknown format: #{@format}"
end
end
end
# Usage
formatter1 = TextFormatter.new(:html)
puts formatter1.format("Hello, World") # => <p>Hello, World</p>
formatter2 = TextFormatter.new(:markdown)
puts formatter2.format("Hello, World") # => **Hello, World**
โ ๏ธ Problems With This Approach
- All formatting logic is stuffed into one method.
- Adding a new format (like XML) means modifying this method (violates Open/Closed Principle).
- Not easy to test or extend individual formatting behaviors.
- Harder to maintain and violates SRP (Single Responsibility Principle).
This sets the stage perfectly to apply the Strategy Pattern, where each format becomes its own class with a clear responsibility.
Instead of writing if/else logic everywhere, use strategy objects:
# Strategy Interface
class TextFormatter
def self.for(format)
case format
when :plain
PlainFormatter.new
when :html
HtmlFormatter.new
when :markdown
MarkdownFormatter.new
else
raise "Unknown format"
end
end
end
# Concrete Strategies
class PlainFormatter
def format(text)
text
end
end
class HtmlFormatter
def format(text)
"<p>#{text}</p>"
end
end
class MarkdownFormatter
def format(text)
"**#{text}**"
end
end
# Usage
def render_text(text, format)
formatter = TextFormatter.for(format)
formatter.format(text)
end
puts render_text("Hello, world", :html) # => <p>Hello, world</p>
puts render_text("Hello, world", :markdown) # => **Hello, world**
๐ Why It’s Better
- Adds new formats easily without changing existing code.
- Keeps formatting logic isolated in dedicated classes.
- Follows the Open/Closed Principle.
๐ณ Strategy Pattern in Rails: Payment Gateway Integration
Here’s a Rails-specific Strategy Pattern example. This example uses a service to handle different payment gateways (e.g., Stripe, PayPal, Razorpay), which are chosen dynamically based on configuration or user input.
Problem:
You want to process payments, but the actual logic differs depending on which gateway (Stripe, PayPal, Razorpay) is being used. Avoid if/else or case all over your controller or service.
โ
Solution Using Strategy Pattern
# app/services/payment_processor.rb
class PaymentProcessor
def self.for(gateway)
case gateway.to_sym
when :stripe
StripeGateway.new
when :paypal
PaypalGateway.new
when :razorpay
RazorpayGateway.new
else
raise "Unsupported payment gateway"
end
end
end
# app/services/stripe_gateway.rb
class StripeGateway
def charge(amount, user)
# Stripe API integration here
puts "Charging #{user.name} โน#{amount} via Stripe"
end
end
# app/services/paypal_gateway.rb
class PaypalGateway
def charge(amount, user)
# PayPal API integration here
puts "Charging #{user.name} โน#{amount} via PayPal"
end
end
# app/services/razorpay_gateway.rb
class RazorpayGateway
def charge(amount, user)
# Razorpay API integration here
puts "Charging #{user.name} โน#{amount} via Razorpay"
end
end
# app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create
user = User.find(params[:user_id])
gateway = params[:gateway] # e.g., 'stripe', 'paypal', etc.
amount = params[:amount].to_i
processor = PaymentProcessor.for(gateway)
processor.charge(amount, user)
render json: { message: \"Payment processed via #{gateway.capitalize}\" }
end
end
โ
Benefits in Rails Context
- Keeps your controller slim and readable.
- Each gateway integration is encapsulated in its own class.
- Easy to test, extend, and maintain (open/closed principle).
- Avoids future code smell like “Shotgun Surgery”.
โ ๏ธ What are Anti-Patterns?
Anti-patterns are poor programming practices or ineffective solutions that seem helpful at first but cause long-term issues like unmaintainable code or hidden bugs.
Common Ruby Anti-Patterns
๐งจ 1. God Object
A class that knows too much or does too much.
class UserManager
def create_user
# logic
end
def send_email
# unrelated responsibility
end
def generate_report
# another unrelated responsibility
end
end
Fix: Follow the Single Responsibility Principle. Split into smaller, focused classes.
๐งจ Shotgun Surgery
Tiny changes require touching many different places in code.
# Example of Shotgun Surgery - Business rule spread across many files
# In order.rb
class Order
def eligible_for_discount?
user.vip? && total > 1000
end
end
# In invoice.rb
class Invoice
def apply_discount(order)
if order.user.vip? && order.total > 1000
# apply discount logic
end
end
end
# In email_service.rb
class EmailService
def send_discount_email(order)
if order.user.vip? && order.total > 1000
# send congratulatory email
end
end
end
Here, the logic user.vip? && total > 1000 is repeated in multiple places. If the discount eligibility rules change (e.g., change the threshold to 2000 or add a new condition), you’ll have to update every occurrence.
โ
Fix: Centralize the Logic
class DiscountPolicy
def self.eligible?(order)
order.user.vip? && order.total > 1000
end
end
Now all files can use DiscountPolicy.eligible?(order), ensuring consistency and easier maintenance.
3. Spaghetti Code
Unstructured and difficult-to-follow code.
def calculate_discount(user, items)
if user.vip?
# deeply nested logic
total = items.sum { |i| i.price }
total * 0.2
else
if user.new_customer?
total = items.sum { |i| i.price }
total * 0.1
else
total = items.sum { |i| i.price }
total * 0.05
end
end
end
This code is hard to read, hard to extend, and violates SRP (Single Responsibility Principle).
Fix: Break into smaller methods, use polymorphism or strategy patterns.
โ
Refactored with Strategy Pattern
# Strategy Interface
class DiscountStrategy
def self.for(user)
if user.vip?
VipDiscount.new
elsif user.new_customer?
NewCustomerDiscount.new
else
RegularDiscount.new
end
end
end
# Concrete Strategies
class VipDiscount
def apply(items)
total = items.sum(&:price)
total * 0.2
end
end
class NewCustomerDiscount
def apply(items)
total = items.sum(&:price)
total * 0.1
end
end
class RegularDiscount
def apply(items)
total = items.sum(&:price)
total * 0.05
end
end
# Usage
def calculate_discount(user, items)
strategy = DiscountStrategy.for(user)
strategy.apply(items)
end
๐ฏ Benefits
- No nested conditionals
- Easy to add new discount types (open/closed principle)
- Each class has one responsibility
- Testable and readable
๐ How to Detect Anti-Patterns in Ruby Code
- Code Smells:
- Long methods
- Large classes
- Repetition (DRY violations)
- Deep nesting
- Code Review Checklist:
- Is each class doing only one thing?
- Can this method be broken down?
- Are we repeating business logic?
- Is the code testable?
- Use Static Analysis Tools:
- Rubocop for style and complexity checks
- Reek for code smells
- Flog for measuring code complexity
๐ How to Refactor Anti-Patterns
- Use Service Objects: Extract complex logic into standalone classes.
- Apply Design Patterns: Choose the right pattern that matches your need (Strategy, Adapter, etc).
- Keep Methods Small: Limit to a single task; ideally under 10 lines.
- Write Tests First: Test-driven development (TDD) helps spot untestable designs.
๐ฏ Conclusion
Understanding design patterns and identifying anti-patterns is crucial for writing better Ruby code. While patterns guide you toward elegant solutions, anti-patterns warn you about common mistakes. With good design principles, automated tools, and thoughtful reviews, your Ruby codebase can remain healthy, scalable and developer-friendly.
Happy Designing Ruby! โจ