Architecture

Design Patterns

Reusable solutions to recurring structural problems, the Gang of Four catalogue and beyond.

Overview

Design patterns are reusable solutions to recurring structural and behavioural problems in software. The Gang of Four (GoF) catalogue (1994) defines 23 patterns across three categories: Creational (object construction), Structural (composition), and Behavioural (interaction). They provide a shared vocabulary for design decisions and encode proven approaches to common problems.

Origin

"Design Patterns: Elements of Reusable Object-Oriented Software" by Gamma, Helm, Johnson, and Vlissides was published in 1994 and remains one of the best-selling programming books ever. The patterns themselves existed in practice before the book; the authors catalogued and named them.

Examples

Observer pattern (Publish/Subscribe)

class EventBus {
  constructor() { this.listeners = {} }

  on(event, fn) {
    this.listeners[event] = this.listeners[event] || []
    this.listeners[event].push(fn)
    return () => this.off(event, fn)  // returns unsubscribe function
  }

  off(event, fn) {
    this.listeners[event] = (this.listeners[event] || []).filter(f => f !== fn)
  }

  emit(event, payload) {
    ;(this.listeners[event] || []).forEach(fn => fn(payload))
  }
}

const bus = new EventBus()
const unsub = bus.on('order:placed', order => sendEmail(order))
bus.emit('order:placed', { id: 42 })
unsub()  // removes this specific listener

Strategy pattern for payment processing

# Strategy: encapsulate interchangeable algorithms
class PaymentProcessor
  def initialize(strategy)
    @strategy = strategy
  end

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

stripe_strategy  = ->(amount) { Stripe::Charge.create(amount: amount) }
paypal_strategy  = ->(amount) { PayPal::Payment.create(value: amount) }
invoice_strategy = ->(amount) { InvoiceService.issue(amount) }

processor = PaymentProcessor.new(stripe_strategy)
processor.charge(1000)

# Runtime switch without if/else chains
STRATEGIES = { stripe: stripe_strategy, paypal: paypal_strategy }
PaymentProcessor.new(STRATEGIES[user.preferred_payment]).charge(total)

Use Cases

  • 01Observer/EventEmitter: decoupling components that react to state changes
  • 02Strategy: swappable algorithms (sorting, payment, export format) without inheritance
  • 03Decorator: adding responsibilities to objects at runtime without subclassing
  • 04Factory/Builder: complex object construction with many optional parameters
  • 05Command: encapsulating operations as objects for undo/redo, queuing, and logging

When Not to Use

  • //Forcing a pattern onto simple code adds indirection without benefit, the Visitor pattern on two objects is overkill
  • //Premature abstraction: reach for a pattern when the problem it solves is actually present
  • //As a substitute for reading the actual requirements, patterns solve structural problems, not domain problems

Technical Notes

  • In Ruby and JavaScript, many GoF patterns are simpler than in Java because functions are first-class values. Strategy is often just a lambda; Command is a proc
  • The most overused patterns: Singleton (usually better handled by dependency injection), and Abstract Factory (premature generalisation)
  • Anti-patterns are as important as patterns: God Object, Shotgun Surgery, Feature Envy, and Lasagna Code (too many layers) are common failure modes
  • Refactoring to Patterns (Kerievsky, 2004) describes how to introduce patterns gradually during refactoring rather than upfront