Add code examples, and documentation
This commit is contained in:
271
docs/observer.md
Normal file
271
docs/observer.md
Normal file
@@ -0,0 +1,271 @@
|
||||
[← 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)
|
||||
Reference in New Issue
Block a user