Observer Pitfalls: Re-entrancy, Ordering, Memory Leaks
What this concept is
Observer is a delivery protocol. Define registration identity, membership during delivery, callback ordering, nested delivery, error reporting and lifetime. A snapshot answers only membership; it does not settle the other questions.
Why it matters here
A duplicate subscription can retain a disposed screen and deliver twice. A nested publication can recurse or interleave events in a surprising order. Exception isolation can keep unrelated views working, while fail-fast behavior can make an incomplete computation visible. Neither is a universal default.
Concrete example
L02 uses this sequential model:
class Subject:
def __init__(self):
self.listeners = {}
self.next_token = 0
self.publishing = False
def subscribe(self, callback):
token = self.next_token
self.next_token += 1
self.listeners[token] = callback
def dispose():
self.listeners.pop(token, None)
return dispose
def publish(self, event):
if self.publishing:
raise RuntimeError('reentrant')
self.publishing = True
failures = []
try:
for callback in tuple(self.listeners.values()):
try:
callback(event)
except Exception as error:
failures.append((type(error).__name__, str(error)))
finally:
self.publishing = False
return failures
Register A then B. A disposes B and registers C during event 1. The trace is A(1), B(1), then on event 2: A(2), C(2). A's registration change takes effect for the next publication. If A recursively publishes, that nested call raises before delivery; if A does not catch it, the outer call reports A's error and continues to B. A finally block resets the flag even after a process-level BaseException; the model does not collect those exceptions.
Common confusion / misconception
Weak references do not define subscription lifetime: collection can remove a listener earlier than the application expects. Explicit disposal establishes ownership, but active snapshots may retain callbacks until delivery ends. Asynchronous delivery avoids this call stack but introduces queues, cancellation, stale payloads and capacity decisions. A queue can still loop forever if events continually generate events. Thread safety is a separate contract.
How to use it
Write a trace before implementing a publisher. Specify whether disposal affects the active snapshot. Return errors or expose a deliberate reporting boundary; silently printing a message is often insufficient for a caller making a decision. Do not infer that notification failure rolls back the publisher's earlier state.
Check yourself
Why must tokens identify registrations rather than callback equality? Why does snapshot membership permit a callback already disposed during this event to run? Which errors leave the outer call and which appear in its result?
Mini drill or application
Complete L02 and P06–P10. Mount/dispose a simulated view three times, retain a second independent registration of the same callback, and show that only the intended registration remains. Then change to immediate suppression of disposed listeners and document the new trace as a policy change, not a refactor.
Read this only if stuck
GoF book index, local excerpt 079-observer-implementation.md. Compare your policy with the Node EventEmitter documentation: actual libraries have explicit ordering and removal semantics; the pattern name alone does not select them. Do not transplant one library's behavior into another.