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
| Role | In this example | Responsibility |
|---|---|---|
| Subject | Base | Keeps 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 Observer | LowLivesWarning | Reacts to a change by reading the subject's new state. |
Ruby implementation
# 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
endIn 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
#callthe observer interface. Blocks, lambdas,method(:handler)and full objects then all work as observers, and nothing needs a shared base class. - The
observerlibrary (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
returnin#lose_livesavoids 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.