CrackedRuby CrackedRuby

Overview

The Observer Pattern establishes a subscription mechanism that allows multiple objects to monitor and react to events or state changes in another object. The pattern separates the object being observed (the subject) from the objects doing the observing (the observers), creating a loose coupling between components that need to stay synchronized.

At its core, the pattern involves two main roles: the subject maintains a list of observers and notifies them automatically when its state changes, while observers register themselves with the subject and define how they respond to notifications. This creates a publish-subscribe relationship where the subject broadcasts changes without knowing the concrete classes of its observers.

The pattern originated from the Model-View-Controller (MVC) architecture where views need to update automatically when model data changes. Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides documented it in "Design Patterns: Elements of Reusable Object-Oriented Software" (1994) as one of the 23 fundamental design patterns.

Observer Pattern solves the problem of maintaining consistency between related objects without creating tight coupling. Instead of objects constantly polling for changes or being hardwired together, the subject pushes notifications to interested parties. This approach reduces dependencies, makes systems more modular, and allows the number of observers to vary dynamically at runtime.

The pattern appears extensively in event-driven systems, GUI frameworks, data binding mechanisms, and distributed systems. Modern reactive programming libraries like RxJS and reactive frameworks build upon the observer pattern's core concepts, extending them with additional operations for transforming and combining event streams.

Key Principles

The Observer Pattern operates on several fundamental principles that define its structure and behavior. Understanding these principles clarifies how the pattern achieves loose coupling while maintaining consistency across distributed components.

Subject-Observer Relationship: The pattern establishes a one-to-many dependency where one subject maintains references to multiple observers. The subject exposes methods for attaching and detaching observers, creating a registry of interested parties. When the subject's state changes, it iterates through this registry and notifies each observer. This relationship remains abstract - the subject operates against an observer interface rather than concrete observer classes, preventing tight coupling.

Push vs Pull Models: Observer notifications follow either a push or pull model. In push-based notification, the subject sends detailed state information to observers as part of the notification. Observers receive all relevant data without making additional requests. In pull-based notification, the subject sends minimal information (often just indicating that a change occurred), and observers query the subject for specific details they need. Push models reduce coupling since observers don't need knowledge of subject interfaces, but may send unnecessary data. Pull models give observers control over what data they retrieve but require observers to know the subject's query interface.

Notification Propagation: When a subject's state changes, it triggers a notification cascade. The subject calls an update method on each registered observer, passing either state information (push) or a reference to itself (pull). Observers process this notification synchronously or asynchronously depending on implementation requirements. Synchronous notification completes all observer updates before the subject method returns. Asynchronous notification queues updates for later processing, allowing the subject to continue immediately.

Registration Lifecycle: Observers must explicitly register and unregister with subjects. Registration adds an observer to the subject's notification list, establishing the subscription. Unregistration removes the observer, terminating the subscription. This explicit lifecycle allows dynamic changes to the observer set at runtime. Objects can become observers when needed and stop observing when no longer interested. The pattern supports multiple observers for a single subject and a single observer watching multiple subjects.

Decoupling Through Abstraction: The pattern achieves loose coupling through abstraction layers. Subjects depend only on an abstract observer interface, not concrete observer implementations. Observers receive notifications through a standardized interface method. This separation allows new observer types to be added without modifying the subject. Similarly, subjects can change internal implementation details without affecting observers as long as they maintain the notification protocol.

State Encapsulation: Subjects encapsulate their internal state and control access to it. State changes occur through subject methods that trigger appropriate notifications. Observers cannot directly modify subject state - they receive read-only information or query interfaces. This encapsulation maintains data integrity and ensures all state changes flow through controlled channels that trigger proper notifications.

Ruby Implementation

Ruby provides multiple approaches for implementing the Observer Pattern, from built-in standard library support to custom implementations using blocks and procs. Each approach offers different trade-offs in terms of flexibility, simplicity, and idiomatic Ruby style.

Standard Observable Module: Ruby's standard library includes an Observable module that provides basic observer pattern functionality. Classes include this module and gain methods for managing observers and triggering notifications:

require 'observer'

class StockTicker
  include Observable
  
  attr_reader :symbol, :price
  
  def initialize(symbol, price)
    @symbol = symbol
    @price = price
  end
  
  def update_price(new_price)
    @price = new_price
    changed
    notify_observers(symbol, price)
  end
end

class StockMonitor
  def update(symbol, price)
    puts "#{symbol} is now $#{price}"
  end
end

ticker = StockTicker.new("AAPL", 150.00)
monitor = StockMonitor.new
ticker.add_observer(monitor)
ticker.update_price(155.50)
# => AAPL is now $155.5

The Observable module requires calling the changed method before notify_observers to mark that state has changed. This two-step process prevents accidental notifications and allows batching multiple state changes before notification.

Custom Observer Implementation: Building a custom observer implementation provides more control over the notification mechanism and allows Ruby-specific idioms like blocks and procs:

class Subject
  def initialize
    @observers = []
  end
  
  def attach(&block)
    @observers << block
  end
  
  def detach(block)
    @observers.delete(block)
  end
  
  def notify(data)
    @observers.each { |observer| observer.call(data) }
  end
end

class TemperatureSensor < Subject
  attr_reader :temperature
  
  def initialize
    super
    @temperature = 20
  end
  
  def update_temperature(temp)
    @temperature = temp
    notify(temperature: temp, timestamp: Time.now)
  end
end

sensor = TemperatureSensor.new

sensor.attach do |data|
  puts "Alert: Temperature is #{data[:temperature]}°C"
end

sensor.attach do |data|
  if data[:temperature] > 30
    puts "WARNING: High temperature detected!"
  end
end

sensor.update_temperature(32)
# => Alert: Temperature is 32°C
# => WARNING: High temperature detected!

This block-based approach feels more idiomatic in Ruby and eliminates the need for separate observer classes. Each observer is simply a block or proc that receives notification data.

Observer with Method Objects: For more complex observers that maintain state or require multiple methods, method objects provide a clean implementation:

class Publisher
  def initialize
    @subscribers = []
  end
  
  def subscribe(subscriber)
    @subscribers << subscriber unless @subscribers.include?(subscriber)
  end
  
  def unsubscribe(subscriber)
    @subscribers.delete(subscriber)
  end
  
  def publish(event_type, data)
    @subscribers.each do |subscriber|
      subscriber.handle_event(event_type, data) if subscriber.respond_to?(:handle_event)
    end
  end
end

class NewsPublisher < Publisher
  def publish_article(article)
    publish(:article_published, article)
  end
  
  def publish_breaking_news(news)
    publish(:breaking_news, news)
  end
end

class EmailSubscriber
  def initialize(email)
    @email = email
  end
  
  def handle_event(event_type, data)
    case event_type
    when :article_published
      puts "Sending article to #{@email}: #{data[:title]}"
    when :breaking_news
      puts "URGENT email to #{@email}: #{data[:headline]}"
    end
  end
end

class AnalyticsSubscriber
  def initialize
    @event_counts = Hash.new(0)
  end
  
  def handle_event(event_type, data)
    @event_counts[event_type] += 1
    puts "Analytics: #{event_type} count = #{@event_counts[event_type]}"
  end
end

publisher = NewsPublisher.new
publisher.subscribe(EmailSubscriber.new("user@example.com"))
publisher.subscribe(AnalyticsSubscriber.new)

publisher.publish_article(title: "Ruby Design Patterns", author: "Developer")
# => Sending article to user@example.com: Ruby Design Patterns
# => Analytics: article_published count = 1

Using Ruby's built-in delegation: Ruby's SimpleDelegator can create observer wrappers that add observation capabilities to existing objects:

require 'delegate'

class ObservableDecorator < SimpleDelegator
  def initialize(obj)
    super
    @observers = []
  end
  
  def add_observer(&block)
    @observers << block
  end
  
  def notify_observers(method_name, *args, result)
    @observers.each do |observer|
      observer.call(method_name, args, result)
    end
  end
  
  def method_missing(method, *args, &block)
    result = super
    notify_observers(method, args, result)
    result
  end
end

class BankAccount
  attr_reader :balance
  
  def initialize(balance)
    @balance = balance
  end
  
  def deposit(amount)
    @balance += amount
  end
  
  def withdraw(amount)
    @balance -= amount if @balance >= amount
  end
end

account = ObservableDecorator.new(BankAccount.new(1000))

account.add_observer do |method, args, result|
  puts "Transaction: #{method} with #{args.inspect}"
end

account.deposit(500)
# => Transaction: deposit with [500]

account.withdraw(200)
# => Transaction: withdraw with [200]

Practical Examples

The Observer Pattern applies to numerous real-world scenarios where objects need to stay synchronized or react to events. These examples demonstrate the pattern in contexts ranging from simple notifications to complex event processing systems.

GUI Event Handling: Graphical user interfaces rely heavily on the observer pattern to handle user interactions. UI components act as subjects that notify observers when users interact with them:

class Button
  def initialize(label)
    @label = label
    @click_handlers = []
  end
  
  def on_click(&handler)
    @click_handlers << handler
  end
  
  def click
    puts "Button '#{@label}' clicked"
    @click_handlers.each { |handler| handler.call }
  end
end

class FormValidator
  def initialize
    @valid = true
  end
  
  def validate
    @valid = false
    puts "Form validation failed"
  end
end

class Analytics
  def track_click(button_name)
    puts "Analytics: Recorded click on #{button_name}"
  end
end

submit_button = Button.new("Submit")
validator = FormValidator.new
analytics = Analytics.new

submit_button.on_click { validator.validate }
submit_button.on_click { analytics.track_click("Submit") }
submit_button.on_click { puts "Submitting form data..." }

submit_button.click
# => Button 'Submit' clicked
# => Form validation failed
# => Analytics: Recorded click on Submit
# => Submitting form data...

Data Model Synchronization: Applications with multiple views of the same data use observers to keep views synchronized when the underlying model changes:

class UserProfile
  attr_reader :name, :email, :status
  
  def initialize(name, email)
    @name = name
    @email = email
    @status = "offline"
    @view_observers = []
  end
  
  def register_view(view)
    @view_observers << view
  end
  
  def update_status(new_status)
    @status = new_status
    notify_views(:status_changed, status: new_status)
  end
  
  def update_email(new_email)
    @email = new_email
    notify_views(:email_changed, email: new_email)
  end
  
  private
  
  def notify_views(event, data)
    @view_observers.each { |view| view.update(event, data) }
  end
end

class ProfileSidebar
  def update(event, data)
    case event
    when :status_changed
      puts "[Sidebar] Status indicator: #{data[:status]}"
    when :email_changed
      puts "[Sidebar] Email: #{data[:email]}"
    end
  end
end

class ProfileDashboard
  def update(event, data)
    case event
    when :status_changed
      puts "[Dashboard] User is now: #{data[:status]}"
    when :email_changed
      puts "[Dashboard] Contact updated: #{data[:email]}"
    end
  end
end

profile = UserProfile.new("Alice", "alice@example.com")
profile.register_view(ProfileSidebar.new)
profile.register_view(ProfileDashboard.new)

profile.update_status("online")
# => [Sidebar] Status indicator: online
# => [Dashboard] User is now: online

profile.update_email("alice.smith@example.com")
# => [Sidebar] Email: alice.smith@example.com
# => [Dashboard] Contact updated: alice.smith@example.com

Event-Driven Architecture: Microservices and distributed systems use observer-like patterns for inter-service communication and event processing:

class EventBus
  def initialize
    @subscribers = Hash.new { |h, k| h[k] = [] }
  end
  
  def subscribe(event_type, &handler)
    @subscribers[event_type] << handler
  end
  
  def publish(event_type, payload)
    @subscribers[event_type].each do |handler|
      handler.call(payload)
    end
  end
end

class OrderService
  def initialize(event_bus)
    @event_bus = event_bus
  end
  
  def create_order(order_data)
    order_id = SecureRandom.uuid
    order = { id: order_id, **order_data, status: "created" }
    
    @event_bus.publish(:order_created, order)
    order
  end
end

class InventoryService
  def initialize(event_bus)
    event_bus.subscribe(:order_created) { |order| reserve_inventory(order) }
  end
  
  def reserve_inventory(order)
    puts "Inventory: Reserving #{order[:quantity]} units for order #{order[:id]}"
  end
end

class NotificationService
  def initialize(event_bus)
    event_bus.subscribe(:order_created) { |order| send_confirmation(order) }
  end
  
  def send_confirmation(order)
    puts "Notification: Sending order confirmation to #{order[:customer_email]}"
  end
end

class AuditService
  def initialize(event_bus)
    event_bus.subscribe(:order_created) { |order| log_event(order) }
  end
  
  def log_event(order)
    puts "Audit: Order #{order[:id]} created at #{Time.now}"
  end
end

event_bus = EventBus.new
order_service = OrderService.new(event_bus)

InventoryService.new(event_bus)
NotificationService.new(event_bus)
AuditService.new(event_bus)

order_service.create_order(
  customer_email: "customer@example.com",
  product_id: "PROD-123",
  quantity: 2
)
# => Inventory: Reserving 2 units for order [uuid]
# => Notification: Sending order confirmation to customer@example.com
# => Audit: Order [uuid] created at [timestamp]

Cache Invalidation: Observers monitor data changes to maintain cache consistency across distributed systems:

class DataStore
  def initialize
    @data = {}
    @change_listeners = []
  end
  
  def add_listener(&listener)
    @change_listeners << listener
  end
  
  def set(key, value)
    @data[key] = value
    notify_change(key, value, :update)
  end
  
  def delete(key)
    value = @data.delete(key)
    notify_change(key, value, :delete)
  end
  
  def get(key)
    @data[key]
  end
  
  private
  
  def notify_change(key, value, operation)
    @change_listeners.each do |listener|
      listener.call(key, value, operation)
    end
  end
end

class CacheLayer
  def initialize
    @cache = {}
  end
  
  def handle_change(key, value, operation)
    case operation
    when :update
      @cache[key] = value
      puts "Cache: Updated #{key} = #{value}"
    when :delete
      @cache.delete(key)
      puts "Cache: Invalidated #{key}"
    end
  end
end

class SearchIndex
  def initialize
    @index = {}
  end
  
  def handle_change(key, value, operation)
    case operation
    when :update
      @index[key] = value
      puts "SearchIndex: Indexed #{key}"
    when :delete
      @index.delete(key)
      puts "SearchIndex: Removed #{key} from index"
    end
  end
end

store = DataStore.new
cache = CacheLayer.new
search_index = SearchIndex.new

store.add_listener { |key, value, op| cache.handle_change(key, value, op) }
store.add_listener { |key, value, op| search_index.handle_change(key, value, op) }

store.set("user:123", { name: "Bob", email: "bob@example.com" })
# => Cache: Updated user:123 = {:name=>"Bob", :email=>"bob@example.com"}
# => SearchIndex: Indexed user:123

store.delete("user:123")
# => Cache: Invalidated user:123
# => SearchIndex: Removed user:123 from index

Design Considerations

Selecting the Observer Pattern requires evaluating several factors that determine whether the pattern fits the specific design problem. The decision involves analyzing coupling requirements, notification complexity, and system architecture.

When to Use Observer Pattern: The pattern suits scenarios where one object's state changes require updates in multiple dependent objects, and the dependencies are dynamic or unknown at compile time. Apply the pattern when the number of observers varies at runtime, when observers need to be added or removed dynamically, or when the subject should not know concrete observer types. The pattern works well in event-driven architectures, reactive systems, and situations requiring loose coupling between components. Use observers when changes in one object trigger cascading updates across multiple objects without creating tight dependencies.

Coupling vs Decoupling Trade-offs: Observer Pattern reduces coupling between subjects and observers through abstraction but introduces coupling through the observer interface. Subjects depend on the observer interface specification, and changes to notification protocols affect all observers. The pattern eliminates direct dependencies between concrete classes but creates runtime dependencies through registration. Pull-based notifications increase coupling since observers need knowledge of subject query interfaces. Push-based notifications reduce coupling but may send unnecessary data. Evaluate whether the reduction in compile-time coupling justifies the introduction of runtime dependencies and interface obligations.

Synchronous vs Asynchronous Notification: Synchronous notification completes all observer updates within the subject's method call. This ensures immediate consistency but blocks the subject until all observers finish processing. Long-running observer operations delay the subject, potentially impacting system responsiveness. Asynchronous notification queues updates for later processing, allowing the subject to continue immediately. This improves subject responsiveness but creates eventual consistency where observers update after a delay. Asynchronous approaches require additional infrastructure for message queuing and handling notification failures. Choose synchronous notification when immediate consistency is required and observer processing is fast. Select asynchronous notification when responsiveness matters more than immediate consistency or when observer processing is time-consuming.

Alternative Patterns: Several patterns address similar problems with different trade-offs. Mediator Pattern centralizes communication between objects through a mediator rather than direct subscriptions. This works better when objects need bidirectional communication and complex interaction logic. Event Aggregator Pattern uses a central event bus for publish-subscribe communication. This scales better for large numbers of event types and subscribers but adds a central dependency. Chain of Responsibility Pattern passes requests along a chain until one handler processes it. This suits scenarios where only one handler should respond rather than notifying all observers. Evaluate whether bidirectional communication, centralized coordination, or selective processing better matches the requirements.

Performance Impact: Observer Pattern introduces overhead from maintaining observer lists and iterating through them during notifications. Large observer lists increase notification time linearly with the number of observers. Each notification requires a method call per observer, adding invocation overhead. Memory overhead includes storage for observer references and any queued notifications in asynchronous implementations. Consider the notification frequency and observer count when evaluating performance. High-frequency notifications to many observers may cause performance issues. Evaluate whether batching notifications or using more efficient data structures reduces overhead. For performance-critical code, measure whether the pattern's overhead is acceptable.

State Consistency Challenges: Notifications triggered mid-update can expose inconsistent intermediate states if subjects notify before completing all state changes. Observers receiving notifications during partial updates may read invalid state combinations. Batching state changes and deferring notifications until changes complete prevents this issue. Circular dependencies occur when observers modify subject state during notification processing, potentially triggering infinite notification loops. Implementing guards that prevent recursive notifications or structuring code to avoid circular dependencies addresses this problem. Transaction-like semantics where notifications occur only after successful completion of all changes maintain consistency.

Common Patterns

Observer Pattern implementations follow several established patterns that address specific scenarios and requirements. These patterns extend the basic observer concept with additional capabilities and constraints.

Subject-Specific Observers: Rather than a generic observer interface, observers implement subject-specific interfaces that receive typed notifications. This approach provides type safety and clearer contracts:

class TemperatureObserver
  def temperature_changed(old_temp, new_temp); end
end

class HumidityObserver  
  def humidity_changed(old_humidity, new_humidity); end
end

class WeatherStation
  def initialize
    @temperature = 20
    @humidity = 50
    @temperature_observers = []
    @humidity_observers = []
  end
  
  def add_temperature_observer(observer)
    @temperature_observers << observer
  end
  
  def add_humidity_observer(observer)
    @humidity_observers << observer
  end
  
  def update_temperature(new_temp)
    old_temp = @temperature
    @temperature = new_temp
    @temperature_observers.each do |obs|
      obs.temperature_changed(old_temp, new_temp)
    end
  end
  
  def update_humidity(new_humidity)
    old_humidity = @humidity
    @humidity = new_humidity
    @humidity_observers.each do |obs|
      obs.humidity_changed(old_humidity, new_humidity)
    end
  end
end

class TemperatureAlert
  def temperature_changed(old_temp, new_temp)
    puts "Temperature: #{old_temp}°C → #{new_temp}°C"
    puts "WARNING: High temperature!" if new_temp > 30
  end
end

station = WeatherStation.new
station.add_temperature_observer(TemperatureAlert.new)
station.update_temperature(35)
# => Temperature: 20°C → 35°C
# => WARNING: High temperature!

Filtered Observers: Observers register interest in specific types of events or conditions rather than receiving all notifications. The subject evaluates filters before notifying observers:

class EventPublisher
  def initialize
    @subscribers = []
  end
  
  def subscribe(filter = nil, &handler)
    @subscribers << { filter: filter, handler: handler }
  end
  
  def publish(event)
    @subscribers.each do |subscriber|
      next if subscriber[:filter] && !subscriber[:filter].call(event)
      subscriber[:handler].call(event)
    end
  end
end

class SecurityEvent
  attr_reader :type, :severity, :message
  
  def initialize(type, severity, message)
    @type = type
    @severity = severity
    @message = message
  end
end

publisher = EventPublisher.new

# Subscribe to only critical events
publisher.subscribe(->(event) { event.severity == :critical }) do |event|
  puts "CRITICAL ALERT: #{event.message}"
end

# Subscribe to authentication events
publisher.subscribe(->(event) { event.type == :authentication }) do |event|
  puts "Auth Event (#{event.severity}): #{event.message}"
end

# Subscribe to all events
publisher.subscribe do |event|
  puts "Log: [#{event.type}] #{event.message}"
end

publisher.publish(SecurityEvent.new(:authentication, :critical, "Failed login attempts exceeded"))
# => CRITICAL ALERT: Failed login attempts exceeded
# => Auth Event (critical): Failed login attempts exceeded
# => Log: [authentication] Failed login attempts exceeded

publisher.publish(SecurityEvent.new(:network, :warning, "Unusual traffic pattern"))
# => Log: [network] Unusual traffic pattern

Weak References: Observers hold weak references to prevent memory leaks when observers are garbage collected but remain registered. Ruby's WeakRef enables this pattern:

require 'weakref'

class ObservableWithWeakRefs
  def initialize
    @observers = []
  end
  
  def add_observer(observer)
    @observers << WeakRef.new(observer)
  end
  
  def notify(message)
    @observers.reject! do |weak_ref|
      begin
        weak_ref.weakref_alive?
        false
      rescue WeakRef::RefError
        true
      end
    end
    
    @observers.each do |weak_ref|
      begin
        observer = weak_ref.__getobj__
        observer.update(message) if observer.respond_to?(:update)
      rescue WeakRef::RefError
        # Observer was garbage collected
      end
    end
  end
end

Observable Collections: Collections that notify observers when elements are added, removed, or modified:

class ObservableArray
  def initialize
    @array = []
    @observers = []
  end
  
  def add_observer(&block)
    @observers << block
  end
  
  def <<(element)
    @array << element
    notify(:added, element, @array.size - 1)
    self
  end
  
  def delete_at(index)
    element = @array.delete_at(index)
    notify(:removed, element, index) if element
    element
  end
  
  def [](index)
    @array[index]
  end
  
  def []=(index, value)
    old_value = @array[index]
    @array[index] = value
    notify(:changed, value, index, old_value)
    value
  end
  
  private
  
  def notify(operation, element, index, old_value = nil)
    @observers.each do |observer|
      observer.call(operation, element, index, old_value)
    end
  end
end

list = ObservableArray.new

list.add_observer do |op, element, index, old_val|
  case op
  when :added
    puts "Added #{element} at position #{index}"
  when :removed
    puts "Removed #{element} from position #{index}"
  when :changed
    puts "Changed position #{index}: #{old_val}#{element}"
  end
end

list << "first"
# => Added first at position 0

list << "second"
# => Added second at position 1

list[0] = "FIRST"
# => Changed position 0: first → FIRST

list.delete_at(1)
# => Removed second from position 1

Change Accumulation: Instead of notifying on each individual change, subjects accumulate changes and send batch notifications:

class BatchSubject
  def initialize
    @observers = []
    @pending_changes = []
    @notifying = false
  end
  
  def add_observer(&block)
    @observers << block
  end
  
  def transaction
    @pending_changes.clear
    yield
    flush_changes unless @pending_changes.empty?
  end
  
  def record_change(change_type, data)
    @pending_changes << { type: change_type, data: data, timestamp: Time.now }
  end
  
  private
  
  def flush_changes
    return if @notifying
    
    @notifying = true
    changes = @pending_changes.dup
    @pending_changes.clear
    
    @observers.each { |observer| observer.call(changes) }
    @notifying = false
  end
end

class BatchedDataStore < BatchSubject
  def initialize
    super
    @data = {}
  end
  
  def bulk_update(updates)
    transaction do
      updates.each do |key, value|
        @data[key] = value
        record_change(:update, { key: key, value: value })
      end
    end
  end
  
  def bulk_delete(keys)
    transaction do
      keys.each do |key|
        value = @data.delete(key)
        record_change(:delete, { key: key, value: value }) if value
      end
    end
  end
end

store = BatchedDataStore.new

store.add_observer do |changes|
  puts "Batch notification: #{changes.size} changes"
  changes.each do |change|
    puts "  #{change[:type]}: #{change[:data][:key]}"
  end
end

store.bulk_update({ "a" => 1, "b" => 2, "c" => 3 })
# => Batch notification: 3 changes
# =>   update: a
# =>   update: b
# =>   update: c

Common Pitfalls

Observer Pattern implementations encounter several recurring problems that cause bugs, performance issues, or unexpected behavior. Understanding these pitfalls helps avoid them during design and implementation.

Memory Leaks from Retained Observers: Subjects hold references to observers in their observer lists. If observers are not explicitly unregistered before becoming unused, the subject's references prevent garbage collection. Long-lived subjects accumulate dead observers that consume memory but never receive meaningful notifications. This commonly occurs in GUI applications where view components register as observers but fail to unregister when destroyed. Applications experience increasing memory usage over time as observer lists grow. Always implement explicit cleanup that unregisters observers when they are no longer needed. Use weak references where appropriate, or implement automatic cleanup through lifecycle hooks.

Notification Loops: Circular dependencies occur when observer A observes subject B, and subject B observes observer A (directly or through intermediate objects). When A's state changes, it notifies B, which modifies its state and notifies A, creating an infinite loop. Less obvious loops occur through chains of observers where notifications eventually circle back. Applications hang or crash with stack overflow errors. Prevent loops by implementing notification guards that track ongoing notifications and skip redundant notifications. Structure code to ensure unidirectional data flow where observers never modify subjects during notification processing. Document notification dependencies and review them during design to identify potential cycles.

Observer Notification Order Dependencies: Observers expecting notifications in a specific order create brittle code when notification order changes. The Observer Pattern provides no guarantees about notification order. Observers receive notifications in the order they were registered, which may change as the application evolves. Code that depends on observer A processing before observer B breaks when registration order changes. Avoid order dependencies by ensuring each observer's update logic is independent. If order matters, the subject should explicitly manage execution order or use different notification mechanisms like priority queues. Document any intentional ordering requirements clearly.

Exception Handling in Observers: When an observer's update method throws an exception during synchronous notification, it prevents subsequent observers from receiving notifications. The subject's notification loop terminates early, leaving some observers unaware of state changes. This creates inconsistent state where some observers updated but others did not. Applications experience unpredictable behavior as parts of the system fall out of sync. Wrap observer notifications in exception handlers that log errors but allow notification to continue for remaining observers:

class RobustSubject
  def initialize
    @observers = []
  end
  
  def add_observer(&block)
    @observers << block
  end
  
  def notify(data)
    @observers.each do |observer|
      begin
        observer.call(data)
      rescue => e
        log_error("Observer notification failed: #{e.message}")
      end
    end
  end
  
  private
  
  def log_error(message)
    puts "ERROR: #{message}"
  end
end

subject = RobustSubject.new

subject.add_observer { |data| puts "Observer 1: #{data}" }
subject.add_observer { |data| raise "Observer 2 failed" }
subject.add_observer { |data| puts "Observer 3: #{data}" }

subject.notify("test")
# => Observer 1: test
# => ERROR: Observer notification failed: Observer 2 failed
# => Observer 3: test

Thread Safety Issues: Multiple threads simultaneously registering observers, triggering notifications, or modifying subject state creates race conditions. Observer lists become corrupted when concurrent modifications occur without synchronization. Notifications may skip observers or notify them multiple times. Subject state may be inconsistent if notifications occur during multi-step state updates. Add synchronization around observer list modifications and notifications using mutexes or other thread-safe data structures. Consider whether notifications should occur within or outside critical sections based on whether observers need consistent state.

Performance Degradation with Large Observer Lists: Notification time scales linearly with observer count. Subjects with hundreds or thousands of observers experience significant delays during notifications. Applications become unresponsive when frequent notifications iterate through large observer lists. Profile notification performance and consider alternatives if observer counts grow large. Batch notifications to reduce frequency, use filtered observers to reduce irrelevant notifications, or replace the observer pattern with more scalable pub-sub systems using message queues.

Forgetting to Call Notification Methods: Subjects that modify state directly without triggering notifications leave observers with stale information. This occurs when state changes through multiple methods and developers forget to add notification calls to new or modified methods. The system appears to work but observers miss updates, causing subtle bugs. Encapsulate all state changes through methods that automatically trigger notifications. Use accessors that call notification methods rather than direct attribute access. Consider implementing state change tracking that detects modifications and triggers notifications automatically.

Reference

Observer Pattern Components

Component Responsibility Key Methods
Subject Maintains observer list and notifies on state changes attach, detach, notify
Observer Receives and processes notifications from subjects update, handle_event
ConcreteSubject Implements specific observable behavior and state state setters that trigger notifications
ConcreteObserver Implements specific response to notifications update implementation with domain logic

Ruby Observable Module Methods

Method Description Usage
add_observer Registers an observer with the subject add_observer(observer)
delete_observer Removes an observer from the subject delete_observer(observer)
delete_observers Removes all observers delete_observers
changed Marks the subject as having changed state changed(state = true)
changed? Checks if the subject has changed changed? returns boolean
notify_observers Sends notifications to all observers notify_observers(*args)
count_observers Returns number of registered observers count_observers returns integer

Notification Models Comparison

Aspect Push Model Pull Model
Data transfer Subject sends complete state data Subject sends minimal notification
Observer coupling Low - observers don't query subject High - observers must know subject interface
Bandwidth Higher - may send unnecessary data Lower - observers request only needed data
Flexibility Less - all observers receive same data More - observers choose what to retrieve
Implementation notify_observers(data) notify_observers(self) then observer queries

Common Implementation Patterns

Pattern When to Use Trade-offs
Block-based observers Simple notifications with minimal state Less structure but more Ruby-idiomatic
Class-based observers Complex observer logic or stateful observers More structure but additional classes
Filtered notifications Observers interested in specific events Reduced unnecessary notifications but filter overhead
Batched notifications High-frequency changes needing efficiency Better performance but delayed consistency
Weak references Long-lived subjects with dynamic observers Prevents leaks but adds complexity

Observer Registration Patterns

# Standard registration
subject.add_observer(observer)

# Block-based registration  
subject.on_event { |data| handle_event(data) }

# Filtered registration
subject.add_observer(filter: ->(e) { e.type == :critical }) { |e| handle(e) }

# Method reference registration
subject.add_observer(method(:handle_update))

# Automatic unregistration
subject.add_observer(observer, auto_remove: true)

Exception Handling Strategies

Strategy Description When to Use
Continue on error Log error and notify remaining observers Independent observers where partial updates acceptable
Stop on error Terminate notification loop on first error Strict consistency requirements
Collect errors Gather all errors and report after notifications Debugging or when multiple failures need visibility
Retry failed Attempt renotification for failed observers Transient failures or network observers

Memory Management Checklist

Issue Solution Implementation
Observer leaks Explicit unregistration Call delete_observer in cleanup methods
Subject leaks Weak references Use WeakRef for observer storage
Circular references Break cycles manually Nil out references in finalizers
Large observer lists Periodic cleanup Remove inactive observers periodically

Thread Safety Approaches

Approach Description Performance Impact
Mutex per operation Lock during add/remove/notify High safety, moderate overhead
Read-write lock Separate read and write locks Better read performance
Copy-on-write Copy list before iteration Fast reads, slower writes
Thread-local observers Separate lists per thread No synchronization needed
Immutable lists Replace entire list on changes Simplest but memory overhead

Performance Optimization Techniques

Technique Benefit Cost
Observer pooling Reuse observer objects Memory savings but added complexity
Lazy notification Defer until next event loop Reduced notification frequency
Priority queues Control notification order Faster critical observers but ordering overhead
Parallel notification Concurrent observer updates Faster for slow observers but synchronization needed
Notification coalescing Combine multiple changes Fewer notifications but batching logic