Files
Design-Patterns/docs/observer.md
2026-08-13 22:47:17 -04:00

9.1 KiB

← Design patterns index

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.
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 the IDisposable subscription handle that fixes the unsubscribe problem.
  • INotifyPropertyChanged powering WPF data binding.
  • 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