9.1 KiB
Observer
Category: Behavioral | Also known as: Event-Subscriber, Listener |
Source: patterns/observer/ObserverPattern/ (+ UpdatedOne/)
Problem
One object's state change must reach an open-ended, changing set of others. Polling (the customer visiting the store daily) wastes the observers' effort; broadcasting to everyone regardless of interest (the store emailing all customers) wastes the publisher's and annoys the uninterested.
Solution
Give the state-holder (Subject/Publisher) a subscription list plus
subscribe/unsubscribe methods. On a state change it walks the list, calling one
common Update() method on each subscriber. All subscribers implement a single
interface, so the publisher never knows their concrete types — new subscriber
types can appear with zero changes to the publisher.
Participants (GoF names → this repo)
- Subject →
Subject— the reusable subscription machinery. - Observer →
IObserver— declaresUpdate(). - ConcreteSubject →
Data— holds the interesting state, callsNotify()on change. - ConcreteObserver →
BarGraph,PieChart— each reacts, and pulls the new value back from the subject.
classDiagram
class IObserver {
<<interface>>
+Update()
}
class Subject {
-List~IObserver~ Observers
+Subscribe(IObserver)
+Unsubscribe(IObserver)
+Notify()
}
class Data {
-int Value
+SetData(int)
+GetData() int
}
class BarGraph {
-Data _data
+Update()
}
class PieChart {
-Data _data
+Update()
}
Subject <|-- Data
IObserver <|.. BarGraph
IObserver <|.. PieChart
Subject o--> IObserver : notifies - abstract coupling
BarGraph --> Data : pulls value on update
PieChart --> Data : pulls value on update
Code
// IObserver.cs — Observer interface
public interface IObserver
{
void Update(); // called when the Subject updates
}
// Subject.cs — reusable subscription machinery, knows nothing about BarGraph or PieChart
public class Subject
{
private readonly List<IObserver> _observers = new();
public void Subscribe(IObserver observer) { _observers?.Add(observer); }
public void Unsubscribe(IObserver observer) { _observers?.Remove(observer); }
public void Notify()
{
foreach (IObserver observer in _observers)
observer.Update(); // abstract coupling: only ever the interface
}
}
// Data.cs — ConcreteSubject; inherits the subscription machinery
public class Data : Subject
{
private int _value;
public Data(int value) { _value = value; }
public void SetData(int value)
{
_value = value;
Notify(); // every mutation fans out
}
public int GetData() { return _value; }
}
// BarGraph.cs — ConcreteObserver
public class BarGraph : IObserver
{
private readonly Data _data;
public BarGraph(Data data) { _data = data; }
public void Update()
{
Console.WriteLine("Bar Graph:");
Console.WriteLine("\tGot an update notification");
Console.WriteLine("\tUpdating my graph with value " + _data.GetData());
Console.WriteLine();
}
}
// PieChart.cs — ConcreteObserver; same shape as BarGraph, its own message text
public class PieChart : IObserver
{
private readonly Data _data;
public PieChart(Data data) { _data = data; }
public void Update()
{
Console.WriteLine("Pie Chart:");
Console.WriteLine("\tGot an update notification");
Console.WriteLine("\tUpdating my chart with value " + _data.GetData());
Console.WriteLine();
}
}
This is the pull style: Update() takes no parameters, so the observer
calls _data.GetData() to fetch what changed. The alternative push style
would be Update(int newValue). Pull keeps the interface stable as the
subject's state grows; push avoids the observer needing a back-reference to the
subject at all. GoF: "The publisher can pass itself as an argument, letting
subscriber fetch any required data directly."
Subscription itself is explicit, not automatic — neither BarGraph nor
PieChart calls Subscribe on itself. Program.cs constructs each observer
and then wires it up separately, so the client controls exactly when an observer
starts listening:
// Program.cs — two independent subscribers on one subject
Data data = new Data(2);
BarGraph bar = new BarGraph(data);
PieChart pie = new PieChart(data);
data.Subscribe(bar);
data.Subscribe(pie);
data.SetData(3);
data.SetData(60);
Note Data never mentions BarGraph or PieChart by name. A third subscriber
type could be added tomorrow without touching Data or Subject — that is the
Open/Closed payoff.
Output (verified)
Bar Graph:
Got an update notification
Updating my graph with value 3
Pie Chart:
Got an update notification
Updating my chart with value 3
Bar Graph:
Got an update notification
Updating my graph with value 60
Pie Chart:
Got an update notification
Updating my chart with value 60
Both subscribers fire on each SetData, and each pulls the current value.
They happen to run in subscription order here because the backing store is a
List<T> — but GoF and Shvets both warn that notification order is not part
of the contract; never write an observer that depends on running before or
after another.
GoF pseudocode (typed events via a dedicated event manager)
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)
class Editor is
private field events: EventManager // composition, not inheritance
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)
interface EventListener is
method update(filename)
class LoggingListener implements EventListener is
method update(filename) is log.write(...)
class EmailAlertsListener implements EventListener is
method update(filename) is system.email(...)
editor.events.subscribe("open", logger)
editor.events.subscribe("save", emailAlerts)
Two upgrades over the repo version worth knowing: event types, so
subscribers pick which events they want; and composition over inheritance —
Editor has an EventManager rather than being a Subject. The repo uses
inheritance (Data : Subject), which burns Data's single base class. Shvets
flags exactly this: put subscription logic in a separate object when the
publisher already has a superclass.
Applicability
- An abstraction has two aspects, one dependent on the other — separate them to vary and reuse them independently.
- A change to one object requires changing others, and you don't know how many.
- An object should notify others without assumptions about who they are.
- Objects should observe others only for a limited time or in specific cases — the subscription list is dynamic.
Pros / Cons
| Pros | Cons |
|---|---|
| Open/Closed — new subscribers without touching the publisher | Subscribers are notified in unspecified order |
| Relationships can be made and broken at runtime | Unexpected updates: one innocuous change can cascade into a storm of updates |
| Abstract, minimal coupling — subject and observers can live in different layers | Forgetting to unsubscribe leaks the observer (see Pitfalls) |
| Supports broadcast: the sender needn't name a receiver |
Seen in the wild (.NET)
- C#
event/delegate— Observer built into the language. IObservable<T>/IObserver<T>(Rx) — the GoF interface pair almost verbatim, plus theIDisposablesubscription handle that fixes the unsubscribe problem.INotifyPropertyChangedpowering WPF data binding.
Related patterns
- One of the four sender/receiver connection strategies alongside Chain of Responsibility, Command, Mediator — Observer's distinguishing trait is dynamic subscribe/unsubscribe.
- Mediator vs. Observer — see the discussion under Mediator; the two are often combined, with the mediator acting as publisher.
← Prev: Memento · All patterns · Next → State