Observer

Learning Objectives

Implement publish–subscribe between a subject and observers.

Handle unsubscription, lifetime, ordering and reentrancy safely.

Recognise memory leaks and thread-safety issues in observer systems.

Prerequisites: Modules 13, 22.

Intuition and Definition

Intent: define a one-to-many dependency so that when one object (the subject) changes state, all its dependents (observers) are notified automatically.

Participants: Subject (keeps a list of observers, notifies them), Observer (callback interface), concrete observers.

Why It Matters

The cluster display, the data logger and the warning chime all react to a speed change — without the speed sensor knowing any of them. Observer decouples producers from consumers.

Syntax and Language Notes

public event EventHandler<double>? SpeedChanged;    // C# events are built-in observers
SpeedChanged?.Invoke(this, 88.0);
// DOM/Node style
emitter.on("speed", (kmh: number) => display.update(kmh));

Complete Example

#include <functional>
#include <iostream>
#include <map>
#include <string>

class SpeedSignal {                                        // Subject
public:
    using Callback = std::function<void(double)>;
    class Subscription {                                    // RAII: unsubscribes in its destructor
        SpeedSignal* s_; int id_;
    public:
        Subscription(SpeedSignal* s, int id) : s_(s), id_(id) {}
        Subscription(Subscription&& o) noexcept : s_(o.s_), id_(o.id_) { o.s_ = nullptr; }
        Subscription(const Subscription&) = delete;
        ~Subscription() { if (s_) s_->observers_.erase(id_); }
    };
    [[nodiscard]] Subscription subscribe(Callback cb) {
        int id = next_++;
        observers_[id] = std::move(cb);
        return Subscription(this, id);
    }
    void set(double kmh) {
        value_ = kmh;
        auto snapshot = observers_;                         // copy: observers may (un)subscribe while notified
        for (auto& [id, cb] : snapshot) cb(kmh);            // ordered by subscription id
    }
    size_t observerCount() const { return observers_.size(); }
private:
    std::map<int, Callback> observers_;
    int next_ = 1;
    double value_
…
set 90 (3 observers)
  display: 90 km/h
  log: speed=90
set 130 (2 observers)
  display: 130 km/h
  chime: overspeed!

Step-by-Step Walkthrough

subscribe returns a Subscription handle; when the handle is destroyed the observer is removed — no forgotten unsubscribes, no dangling callbacks.

Observers are notified in subscription order (a std::map keyed by an increasing id).

set iterates over a snapshot, so an observer that subscribes or unsubscribes during notification doesn't invalidate the loop (reentrancy safety).

After the logger's scope ends, only two observers remain.

sequenceDiagram
  participant S as SpeedSignal
  participant D as Display
  participant C as Chime
  S->>D: notify(130)
  S->>C: notify(130)
  C-->>S: (prints overspeed)

Common Mistakes

Lapsed listener leak: observers never unsubscribe; the subject keeps them (and everything they reference) alive forever — the most common GC-language leak.

Dangling observers in C++: a destroyed object remains registered → crash on the next notification.

Modifying the observer list while iterating it. Assuming notification order unless the subject guarantees it.

Long or blocking work in callbacks — the subject's set() becomes slow; queue work instead.

Thread safety: notifying from one thread while another subscribes requires locking (and never holding the lock while calling callbacks, to avoid deadlocks).

Trade-offs and Alternatives

Observer is synchronous and in-process. For cross-thread or cross-service communication use message queues or event buses (Module 61, 76), which add buffering, back-pressure and persistence. Reactive streams (RxJS, Rx.NET) add composition operators on top of Observer.

Real-World Use

GUI event handlers, model–view bindings, C# events, DOM events, signal/slot systems (Qt), AUTOSAR RTE sender/receiver notifications, sensor value subscriptions in cluster software.

Practice

Q1. (Easy) What does the subject know about its observers?

Answer: Only that they implement the observer interface/callback signature.

Q2. (Medium) Why iterate over a copy of the observer list?

Answer: Callbacks may subscribe or unsubscribe during notification, which would invalidate iterators of the live container.

Q3. (Advanced) In C#, why do event subscriptions cause memory leaks, and how do you avoid them?

Answer: The publisher's event holds a delegate referencing the subscriber, so a long-lived publisher keeps short-lived subscribers reachable. Unsubscribe (-=) when the subscriber's lifetime ends (e.g. in Dispose), use weak event patterns, or make the subscription an IDisposable handle.

Interview Question

Q1. "What problems do you need to solve in a production Observer implementation?"

Answer: Lifetime (unsubscribe automatically — RAII handles or weak references), reentrancy (observers changing subscriptions or triggering new notifications during a notification), ordering guarantees, exception handling (one failing observer shouldn't stop the others, or the policy must be defined), thread safety (lock the list, but don't call observers while holding the lock), and performance (slow observers blocking the publisher — consider async dispatch).

Quick Quiz

Q1. What is a lapsed listener?

Answer: An observer that's no longer needed but still registered, kept alive by the subject.

Q2. Is Observer synchronous by default?

Answer: Yes — the subject calls observers directly.

Q3. Built-in observer mechanism in C#?

Answer: Events (event + delegates).

Summary

Observer decouples a subject from many dependents through notifications.

Manage lifetime, reentrancy, ordering, exceptions and threads explicitly.

Related: Modules 13, 47, 61, 76.