272 lines
9.1 KiB
Markdown
272 lines
9.1 KiB
Markdown
[← Design patterns index](README.md)
|
|
|
|
# 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` — declares `Update()`.
|
|
- **ConcreteSubject** → `Data` — holds the interesting state, calls `Notify()`
|
|
on change.
|
|
- **ConcreteObserver** → `BarGraph`, `PieChart` — each reacts, and pulls the new
|
|
value back from the subject.
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```csharp
|
|
// 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
|
|
}
|
|
}
|
|
```
|
|
|
|
```csharp
|
|
// 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:
|
|
|
|
```csharp
|
|
// 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 the `IDisposable` subscription handle that fixes the unsubscribe problem.
|
|
- `INotifyPropertyChanged` powering 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](memento.md) · [All patterns](README.md) · **Next →**
|
|
[State](state.md)
|