Observer

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Also known as: Publish-Subscribe, Dependents

Participants

RoleIn this exampleResponsibility
SubjectBaseKeeps a list of observers and notifies them when it loses lives.
Observer#call(base)The interface every observer shares. Objects, lambdas and blocks all qualify.
Concrete ObserverLowLivesWarningReacts to a change by reading the subject's new state.

Ruby implementation

observer.rb
# The Subject: the player's base. It tells its observers whenever it loses lives.
class Base
  attr_reader :lives

  def initialize(lives)
    @lives = lives
    @observers = []
  end

  # Observers are anything that responds to #call: an object, a lambda or a block.
  def subscribe(observer = nil, &block)
    @observers << (observer || block)
    self
  end

  def unsubscribe(observer)
    @observers.delete(observer)
    self
  end

  def lose_lives(count)
    return if count.zero?

    @lives = [lives - count, 0].max
    @observers.each { |observer| observer.call(self) }
  end
end

# A Concrete Observer: warns the player when lives run low.
class LowLivesWarning
  attr_reader :messages

  def initialize(threshold)
    @threshold = threshold
    @messages = []
  end

  def call(base)
    @messages << "Only #{base.lives} lives left!" if base.lives <= @threshold
  end
end

In use

warning = LowLivesWarning.new(5)
base = Base.new(20)
base.subscribe(warning)
base.subscribe { |b| puts "Lives: #{b.lives}" }

base.lose_lives(10)  # prints "Lives: 10"
base.lose_lives(6)   # prints "Lives: 4"

warning.messages     # => ["Only 4 lives left!"]

The UI no longer has to compare the game with how it looked last frame to spot what changed. It subscribes, and gets told.

Notes

Ruby idioms

  • Make #call the observer interface. Blocks, lambdas, method(:handler) and full objects then all work as observers, and nothing needs a shared base class.
  • The observer library (include Observable, add_observer, changed, notify_observers) was in the standard library for years. Since Ruby 3.4 it’s a bundled gem, so declare it in your Gemfile if you use it.
  • Only notify on real change. The early return in #lose_lives avoids notifying observers when nothing happened.

Reach for it when

  • A change to one object should trigger updates in others, and the subject shouldn’t know who they are: UI updates, sound effects, achievements, logs.

Watch for

  • Observers that never unsubscribe keep objects alive (memory leaks) and keep firing after they’re needed.
  • Don’t let one observer depend on another having run first. The order they’re notified in is an accident of when they subscribed.

The subject doesn’t know what its observers do with the news.