Skip to main content

Observer

complexitypopularity

Let objects subscribe to events on a publisher and get notified automatically, without the publisher knowing who they are.

The problem

A Customer desperately wants the new phone the Store is about to stock. Option one: they visit the store every day and check. Almost every trip is wasted. Option two: the store emails every customer about every new product. The interested customer is happy; everyone else calls it spam.

Either the subscriber wastes effort polling, or the publisher wastes effort (and goodwill) broadcasting. In code the same tension shows up as hard-wired reactions.

Every new reaction means reopening saveFile(). The editor slowly becomes a switchboard that knows about logging, email, status bars, and whatever next month brings.

The solution

Name the sides: the object with interesting state is the publisher; everyone tracking it is a subscriber. The pattern adds a subscription mechanism to the publisher - just a list of subscriber references and a couple of subscribe / unsubscribe methods. When something notable happens, the publisher walks the list and calls the notification method on each subscriber.

The crucial move: all subscribers implement the same interface (usually a single update(context) method), and the publisher talks to them only through it. New subscriber classes can appear without the publisher changing a line.

ApplicationEditor (publisher)EventManagerLoggingListenersubscribe("save", logger)1saveFile()2file.write()3notify("save", name)4update(name)5log.write(...)6
  1. 1At startup the application wires listeners to the events they care about. The editor is not involved.
  2. 2Someone saves a file. Ordinary business logic, nothing pattern-shaped yet.
  3. 3The editor does its actual job first. Events are a side effect, not the main act.
  4. 4Then it tells its EventManager that something interesting happened. It has no idea who is listening.
  5. 5The manager walks its list and calls update() on every subscriber of "save" - through the interface, never the concrete class.
  6. 6Each subscriber reacts its own way. Add an email alert tomorrow: subscribe it, done, editor untouched.

Structure

The editor example delegates list management to a helper. That is a common upgrade: the subscription machinery looks identical for every publisher, so it gets extracted, and a class that already has a superclass can still become a publisher by composition.

«interface»EventListenerupdate(filename)SUBSCRIBEREventManagerlisteners: mapsubscribe(type, l)unsubscribe(type, l)notify(type, data)SUBSCRIPTIONEditorevents: EventManageropenFile(path)saveFile()CONCRETELoggingListenerupdate(filename)CONCRETEEmailAlertsListenerupdate(filename)CONCRETE
implementsuses

Code

Same example three ways: an editor notifying services about file events.

// Every reaction is welded into the business logic.
class Editor is
method saveFile() is
file.write()
 
// New requirement? Come back here and edit this method.
log.write("Saved: " + file.name)
system.email("admin@example.com", "Saved: " + file.name)
statusBar.refresh()
// ...and every new reaction grows this list forever.
// The editor does its job and announces the event. That's it.
class Editor is
public field events: EventManager
 
method saveFile() is
file.write()
events.notify("save", file.name)
 
// Reactions live in subscribers, wired up elsewhere:
editor.events.subscribe("save", new LoggingListener(...))
editor.events.subscribe("save", new EmailAlertsListener(...))
// Next month's requirement is one more subscribe() line.
// The subscription machinery, reusable for any publisher.
class EventManager is
private field listeners: hash map of event types and listeners
 
method subscribe(eventType, listener) is
listeners.add(eventType, listener)
 
method unsubscribe(eventType, listener) is
listeners.remove(eventType, listener)
 
method notify(eventType, data) is
foreach (listener in listeners.of(eventType)) do
listener.update(data)
 
// The concrete publisher: real business logic plus one field.
class Editor is
public field events: EventManager
private field file: File
 
constructor Editor() is
events = new EventManager()
 
method openFile(path) is
this.file = new File(path)
events.notify("open", file.name)
 
method saveFile() is
file.write()
events.notify("save", file.name)
 
// The one interface every subscriber implements.
interface EventListener is
method update(filename)
 
class LoggingListener implements EventListener is
private field log: File
private field message
 
method update(filename) is
log.write(replace('%s', filename, message))
 
class EmailAlertsListener implements EventListener is
private field email: string
private field message
 
method update(filename) is
system.email(email, replace('%s', filename, message))
 
// Wiring happens at runtime, away from the publisher.
class Application is
method config() is
editor = new Editor()
logger = new LoggingListener("/path/to/log.txt",
"Someone has opened the file: %s")
editor.events.subscribe("open", logger)
 
emailAlerts = new EmailAlertsListener("admin@example.com",
"Someone has changed the file: %s")
editor.events.subscribe("save", emailAlerts)

When to use it

  • Changes in one object require reactions in others, and you cannot know the full set of reactors in advance - GUI events are the canonical case.
  • Some objects should watch others only temporarily; the dynamic subscription list makes joining and leaving cheap.

Pitfalls

  • Random notification order. Subscribers are notified in whatever order the list happens to hold them. Logic that depends on order is a latent bug.
  • The lapsed listener. Forgetting to unsubscribe keeps dead objects alive (in GC languages) or calls into freed ones (elsewhere). Unsubscribe is part of the subscriber's lifecycle, not an optional courtesy.
  • Notification storms. A publisher that fires on every tiny mutation can bury the system in updates; batch or debounce when state changes in bursts.

Don't confuse it with

  • Mediator. Mediator's goal is eliminating mutual dependencies among components by routing communication through one hub; Observer's goal is dynamic one-way subscriptions. The confusion is earned: a Mediator is frequently implemented with an Observer inside, hub as publisher, components as subscribers.
  • Chain of Responsibility. CoR passes a request along a chain until one receiver handles it; Observer hands the event to every receiver that subscribed.
  • Pub/Sub middleware. Message brokers (Kafka, SNS) are the same idea grown up and moved out of process: the broker plays EventManager for entire services.

Check yourself

Question 1 of 5

What is the one thing the publisher is allowed to know about its subscribers?