Home Design Patterns Observer
Observer
Behavioral Gang of Four“Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.”
Gamma, Helm, Johnson & Vlissides, p. 293Purpose
Observer lets one object announce that something happened without knowing who cares. Interested objects register themselves; the announcing object keeps a list and walks it. The whole point is the direction of the dependency: the observers know about the subject, and the subject knows nothing about the observers beyond a single interface.
That asymmetry is what makes the pattern worth having. Without it, every new consumer of an event is an edit to the producer. With it, the producer was written once and never has to be opened again, no matter how many things end up listening.
Design
The Subject holds a collection rather than a single reference, and offers
attach and detach so the collection can change while
the program runs. Its notify operation iterates the collection and
calls one method on each entry. Everything else in the pattern is a consequence
of those three operations.
There is a real design decision hidden in that dashed arrow. A push model sends the changed data with the notification, which keeps observers ignorant of the subject but forces the subject to guess what everyone needs. A pull model sends only "something changed" and lets each observer query, which is more flexible and reintroduces a dependency on the subject's interface. Most systems end up somewhere in between, sending a small event object that carries the common case and a reference for the rest.
Example
The Autobot Robotics embedded controller had grown a habit. Every time something new needed to know when an arm finished a move, somebody added a call to the bottom of the motion routine. It called the operator display, then the cycle-count store, then the vision system's settle detector. Each of those calls was a line in a module that had to be reviewed and requalified whenever it changed, so a two-line addition for the reporting team cost a week of somebody else's time.
The fix was to make the controller a subject. It publishes arm events to a list of observers and no longer names any of them. The operator display, the cycle logger, the safety watchdog and the vision system all attach at start-up. Adding a new consumer is now a change to the configuration that builds the observer list, and the motion module has not been reopened since.
The first version of that change was wrong in a way worth recording. It called the observers inline, from the servo update loop, and the audit logger's file write occasionally missed the loop deadline. Notification is now a push onto a queue that a lower priority thread drains. The observer list works exactly as the book describes; what the book does not tell you is that in a real-time context you cannot let an unknown number of unknown objects run inside your control loop.
interface ArmObserver
{
void onArmEvent(ArmEvent e);
}
class ArmController
{
private List<ArmObserver> observers = new List<ArmObserver>();
private ArmState state;
public void attach(ArmObserver o) { observers.Add(o); }
public void detach(ArmObserver o) { observers.Remove(o); }
public void setState(ArmState s)
{
state = s;
notify(new ArmEvent(state));
}
private void notify(ArmEvent e)
{
// Iterate a copy. An observer is allowed to detach itself
// in response to the very event it is being handed.
foreach (ArmObserver o in observers.ToArray())
o.onArmEvent(e);
}
}
class SafetyWatchdog : ArmObserver
{
public void onArmEvent(ArmEvent e)
{
// trip the interlock if the event says so
}
}
class MotionLogger : ArmObserver
{
public void onArmEvent(ArmEvent e)
{
// append the event to the audit file
}
}
ToArray() in
notify is not decoration; without it, an observer that detaches
during notification corrupts the iteration.Seen in the Wild
The browser DOM's addEventListener is Observer with the subject
being every node in the document. Java shipped
java.util.Observable and java.util.Observer in version
1.0 and deprecated both in Java 9, largely because the interface was too weak to
be useful: no ordering guarantee, no thread safety, and a single untyped
argument. .NET promoted the pattern into the language with the
event keyword, and later added the explicit
IObservable<T> and IObserver<T> pair that
Reactive Extensions is built on. Qt's signals and slots and Node's
EventEmitter are the same shape again. Further down the stack,
PostgreSQL's LISTEN and NOTIFY are Observer
implemented across a connection boundary.
Comparisons
- Mediator also stops components referring to each other directly, but a mediator knows all of its colleagues by name and routes between them. A subject broadcasts in one direction to a list it cannot identify.
- Command is what you get when you change what travels. A notification reports that something happened; a command asks for something to happen. Systems that log events for replay usually end up needing both.
- Chain of Responsibility also hands something to a series of objects, but it stops at the first one that deals with it. Observer expects every listener to see every event.
- Singleton is how a global event bus normally gets reached, and it is where Observer designs most often go wrong. A bus that anything can publish to and anything can subscribe to has no dependency structure left to reason about.
- Memento pairs with it when the observers need history rather than just the current value.
Why It Exists
My take
Observer exists because the alternative is a producer that has to be edited every time a consumer appears. That sounds like a mild inconvenience until the producer is a module under change control, or a library you shipped, or a real-time routine that somebody has to re-review line by line. The cost of editing a file is not the typing. Observer moves the edit to the place where the new requirement actually is.
What I find more interesting is that it is one of very few patterns the
industry decided to put in the languages themselves. C# has
event. JavaScript has listeners in the runtime. Qt added
keywords and a code generator for it. When a pattern is common enough that
language designers pay syntax for it, that is a strong signal it was
describing something structural rather than a coding trick. It also means you
should almost never write the list-and-loop version by hand in a language that
already has one.
The honest counterweight is that Observer buys decoupling with traceability. In a directly-wired system you can read the call site and see what happens next. In an observer system you cannot, because what happens next depends on what registered, in what order, at runtime. I have spent more hours than I would like tracking down "who is listening to this" in systems where the answer was assembled from three configuration files. The pattern is right far more often than it is wrong, but it is not free, and the price is paid by whoever debugs it rather than whoever wrote it.
Criticisms
The best-known failure is the lapsed listener. An observer registers, the
thing that owned it goes away, and nobody calls detach. The
subject's list is now the only reference keeping the observer alive, so it is
never collected and it keeps receiving events and doing work on behalf of a
component that no longer exists. In a long-lived process this is a slow memory
leak with a matching slow performance leak, and it is entirely invisible until
the machine has been running for a fortnight.
Ordering is unspecified and people rely on it anyway. Nothing in the pattern says observers are notified in registration order, and even when an implementation happens to do that, depending on it couples two observers that were supposed to know nothing about each other. Related to that is reentrancy: an observer that modifies the subject during notification triggers another notification, and cascading updates can either loop or produce an ordering where some observers see a state that never officially existed.
Finally, the pattern says nothing about threads, and that omission is where most real bugs come from. If notification runs on the publisher's thread, then every observer is now code executing in that thread's context, with that thread's deadlines and that thread's locks held. If notification is queued instead, observers see a consistent but stale world, and code that assumes it can read the subject when the event arrives is racing. Neither answer is wrong, but the pattern gives you no help choosing, and the choice is the hard part.