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 |