Observer Pattern


  • Description: Define a one-to-many dependency so when a subject changes state, all dependents (observers) are notified automatically — the foundation of event-driven and reactive programming.
  • My Notion Note ID: K2C-2-18
  • Created: 2026-05-22
  • Updated: 2026-05-22
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Intent

  • Publish/subscribe at the object level.
  • Subject maintains a list of Observers; on state change, calls each Observer's update.
  • Subject doesn't know concrete Observer types — only the interface.
  • Foundation of MVC, GUI events, reactive frameworks, signal/slot, event sourcing notifications.

Typical uses:

  • GUI — model changes → views redraw.
  • Domain events — order placed → inventory + email + analytics react.
  • Signals/slots — Qt, boost::signals2.
  • Reactive streams — Rx, observable sequences.
  • Logging / metrics — multiple sinks subscribe to log events.

2. Structure

Roles:

  • Subject — interface for attach(observer), detach(observer), notify(). Holds observer list.
  • ConcreteSubject — has state; calls notify() after mutation; exposes get_state().
  • Observer — interface with update(subject) (or typed update(event)).
  • ConcreteObserver — reacts; may pull state from subject or read pushed event.
ConcreteSubject ──notify()──► [Observer1, Observer2, Observer3]
       ▲                              │
       └────── pull state ◄───────────┘  (push model: state passed in update)

Push vs pull:

  • Push — subject passes the changed data to update(event).
  • Pull — subject only notifies; observers query subject for current state.
  • Push = less coupling but rigid event types; pull = more flexible but observers depend on subject's getters.

3. C++ Example

3.1 Classic OO Observer

#include <vector>
#include <algorithm>
#include <memory>
#include <iostream>
#include <string>

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(const std::string& event) = 0;
};

class Subject {
    std::vector<std::weak_ptr<Observer>> obs_;
public:
    void attach(std::shared_ptr<Observer> o) { obs_.push_back(o); }
    void detach(const std::shared_ptr<Observer>& o) {
        obs_.erase(
            std::remove_if(obs_.begin(), obs_.end(),
                [&](const std::weak_ptr<Observer>& w) {
                    return w.expired() || w.lock() == o;
                }),
            obs_.end());
    }
    void notify(const std::string& event) {
        std::vector<std::shared_ptr<Observer>> live;
        for (auto& w : obs_) if (auto s = w.lock()) live.push_back(s);
        for (auto& s : live) s->update(event);
    }
};

class Logger : public Observer {
public:
    void update(const std::string& event) override {
        std::cout << "[log] " << event << "\n";
    }
};

class Alerter : public Observer {
public:
    void update(const std::string& event) override {
        if (event.find("ERROR") != std::string::npos) std::cout << "[alert!] " << event << "\n";
    }
};

int main() {
    Subject s;
    auto log = std::make_shared<Logger>();
    auto alert = std::make_shared<Alerter>();
    s.attach(log);
    s.attach(alert);

    s.notify("INFO startup");
    s.notify("ERROR disk full");
}

weak_ptr lets observers die without the subject holding them alive (Lapsed Listener problem solved).

3.2 boost::signals2 — production-grade signal/slot

For real projects, write Observer with boost::signals2:

#include <boost/signals2.hpp>
#include <iostream>

namespace bs = boost::signals2;

int main() {
    bs::signal<void(const std::string&)> sig;

    bs::connection c1 = sig.connect([](const std::string& e) {
        std::cout << "[log] " << e << "\n";
    });
    bs::connection c2 = sig.connect([](const std::string& e) {
        if (e.find("ERROR") != std::string::npos) std::cout << "[alert!] " << e << "\n";
    });

    sig("INFO startup");
    sig("ERROR disk full");

    c1.disconnect();
    sig("INFO ignored by logger");
}

signals2 benefits:

  • Thread-safe by default — connect/disconnect/emit can be called concurrently.
  • Scoped connections (scoped_connection) auto-disconnect on destruction → RAII subscriptions.
  • Slot tracking — attach shared_ptr to a slot; slot auto-disconnects when target expires.
  • Combiners — combine return values from all slots (default ignores returns).

3.3 Thread-safety pitfall — concurrent subscribe + notify

The naive Observer in § 3.1 is not thread-safe. Two failure modes:

  • Concurrent attach + notifyvector reallocates while iterated → UB.
  • Observer detaches itself during update — modifies obs_ mid-iteration → UB.

Fixes:

  • Mutex around obs_ for attach/detach; copy under lock then iterate unlocked (as in § 3.1).
  • Use boost::signals2 — handles both cases internally.
  • Use a lock-free queue and defer notifications to a single dispatcher thread.
#include <mutex>

class SafeSubject {
    std::vector<std::weak_ptr<Observer>> obs_;
    mutable std::mutex m_;
public:
    void attach(std::shared_ptr<Observer> o) {
        std::lock_guard lk(m_);
        obs_.push_back(std::move(o));
    }
    void notify(const std::string& e) {
        std::vector<std::shared_ptr<Observer>> live;
        {
            std::lock_guard lk(m_);
            for (auto& w : obs_) if (auto s = w.lock()) live.push_back(s);
        }
        for (auto& s : live) s->update(e);  // call outside lock — avoid re-entrancy deadlock
    }
};

Critical: never call observer callbacks while holding the subject's lock — observer may try to attach/detach → deadlock.


4. When to Use / When Not To

Use when:

  • Change in one object requires notifying many others, exact set unknown at compile time.
  • Observers can come and go at runtime.
  • Subject should not be coupled to observer types.
  • Building event-driven, reactive, or pub-sub architectures.

Don't use when:

  • One subject, one observer, fixed at compile time → direct call.
  • Strict ordering or guaranteed delivery needed → use a real message queue.
  • Synchronous notification blocks the subject for too long → switch to async / queue.
  • Notification graph becomes a dependency mess — consider Mediator to centralize.

5. Variants and Pitfalls

Variants:

  • Push vs pull model — see § 2.
  • Typed eventsObserver<Event> template; one observer per event type.
  • Signal/slot — Qt, boost::signals2, sigslot. Connections as values; RAII unsubscribe.
  • Reactive streams — Observer + lazy + composable operators (Rx, ReactiveX, C++20 ranges-like).
  • Event sourcing — Observers receive a stream of immutable events.
  • Hot vs cold observable — hot: events fire whether or not anyone subscribes. Cold: starts producing on subscription.

Pitfalls:

  • Lapsed Listener — observer destroyed without detaching → subject calls a dangling pointer. Fix: weak_ptr, or RAII connection objects, or signal tracking.
  • Order of notification — undefined unless documented. Don't rely on insertion order.
  • Re-entrant notify — observer mutates subject during update → recursive notify, infinite loop, or invalidation. Defer side effects.
  • Concurrent subscribe + notify — see § 3.3. Race or UB without locking; never call slots under the subject's lock.
  • Cascading notifications — A's update fires B, B's fires C, C's fires A. Detect cycles or break with a flag.
  • Memory churn — fine-grained events → many tiny allocations. Batch, coalesce, or pool.
  • Hidden coupling via shared state — observers reading subject state synchronously serialize behavior; mutating subject from inside update reintroduces coupling.

6. Related Patterns

  • Observer vs Mediator — the canonical contrast:
    • Observer = distribute. Subject broadcasts; observers subscribe independently. Peers don't know each other. One-to-many. Notification logic lives at the subject.
    • Mediator = centralize. Hub object coordinates components; components talk to the hub. Many-to-many collapses to star. Coordination logic lives in the hub.
    • They combine: Mediator is often implemented using Observer internally (Mediator subscribes to events from each colleague, dispatches commands).
  • Observer vs Publish-Subscribe (event bus) — Pub-sub is Observer generalized:
    • Observer — subject knows the observer list directly.
    • Pub-sub — broker / event bus sits between; publisher and subscriber don't reference each other. = Mediator + Observer hybrid.
  • Observer vs Command — Observer notifies; Command encapsulates the action to take. Observer's update may instantiate and run Commands.
  • Observer vs Iterator — Iterator pulls (consumer asks for next), Observer pushes (producer fires). Same dataflow, opposite direction.
  • Observer vs Chain of Responsibility — CoR routes one request to one handler in order; Observer fans out one event to many handlers, all run, order undefined.
  • Observer vs Memento — orthogonal; Observers may receive Mementos as the payload of a state-change event.

7. References