Behavioral Patterns
Observer
Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
The Observer Pattern (also known as the Publish-Subscribe pattern) is a behavioral design pattern that defines a one-to-many dependency between objects. When one object (the Subject or Observable) updates its state, all its registered dependents (Observers) are notified automatically. This is a foundational pattern for designing event-driven systems and real-time dashboards.
1. Learning Objectives
- Identify one-to-many state dependencies and refactor them into event publisher/subscriber relationships.
- Differentiate between the structural and protocol designs of Push and Pull observer notifications.
- Diagnose and prevent Lapsed Listener memory leaks using weak references.
- Evaluate the performance of synchronous broadcasts versus asynchronous queue-based dispatches.
- Construct thread-safe publishers in Java, Python, and C++ (utilizing weak pointer cleanups).
2. Problem & Naive Solution
Suppose you are building a real-time financial tracking dashboard. You have a StockMarket class that aggregates live price changes of stock symbols. Several display boards need to update when stock prices fluctuate:
MobileAppAlert: Triggers push messages if a stock drops below a target.TradingConsole: Refreshes live ticker widgets.EODReportGenerator: Logs price changes for end-of-day analytics.
The Naive Solution
In a naive architecture, the observers pull price updates in a busy-wait thread loop, or the subject class is directly coupled to all target dashboard components:
This direct-access design has significant design flaws:
- Violates Open/Closed Principle: Adding a new dashboard component (e.g.
EmailAlertSystem) forces you to modify the coreStockMarketclass. - Tight Coupling: The market class depends directly on concrete client panels, preventing reuse of the market component in console-only services.
- Untestability: You cannot run unit tests on the market component without instantiating full UI panels.
3. Issues
Direct coupling blocks scalability. In busy systems, updating thousands of clients synchronously inside the main publisher thread stalls execution, leading to dropped packets or connection timeouts.
4. Pattern Introduction & UML
The Observer Pattern addresses these issues by decoupling the event publisher from subscribers. The subject (StockMarket) exposes methods to register and unregister observers. When a price changes, the subject loops over its collection of abstract Observer references and calls their update() method, remaining decoupled from concrete dashboard details.
UML: Observer Stock Ticker
5. Participants
- Subject (
Subject): The interface for registering, unregistering, and notifying observers. - Concrete Subject (
StockMarket): Stores state and notifies observers of changes. - Observer (
Observer): The interface defining the update callback method contract. - Concrete Observer (
MobileAppAlert,TradingConsole): Implements the update callback to refresh display widgets or trigger alarms.
6. Theory (Push vs. Pull Notification Models)
You can design the notification protocol using two models:
- Push Model (Subject-Driven): The subject broadcasts detailed state update arguments inside the callback method (e.g.
update(symbol, price)). - *Pros*: Simple for observers. They receive the data immediately. - *Cons*: Brittle. Adding new state variables forces updates to theObserverinterface signature. - Pull Model (Observer-Driven): The subject notifies observers by passing a reference to itself, or nothing at all (e.g.,
update()). Observers query the subject's getter methods to retrieve the specific data they need. - *Pros*: Highly decoupled. Observers choose what to retrieve. - *Cons*: Requires observers to maintain a reference to the concrete subject class.
7. Syntax Explanation
Syntax construction tips for observers:
- Java: Declares thread-safe listener lists (
CopyOnWriteArrayList), which allow safe registrations and unregistrations during active broadcast loops. - Python: Uses sets to store observers (
self._observers = set()) to prevent duplicate subscriptions. - C++: Employs
std::weak_ptrlists to avoid circular reference dependencies and prevent memory leaks.
8. Step-by-Step Implementation
- Step 1: Define the
Observerinterface with theupdate()callback. - Step 2: Define the
Subjectinterface outliningsubscribe,unsubscribe, andnotifymethods. - Step 3: Implement
ConcreteSubjectwith a thread-safe list to hold registered observers. - Step 4: Implement
ConcreteObserverclasses implementing theObserverinterface. - Step 5: Register observers with the subject instance, then trigger state changes to test the system.
9. Complete Code (Mini Project)
10. Code Walkthrough
Let's review the decoupled publisher design:
- Unified Delegation: The
StockMarketclass interacts only with the abstractStockObserverinterface, keeping it decoupled from concrete alert logic. - Safe Concurrent Iteration: In the Java example, using a
CopyOnWriteArrayListpreventsConcurrentModificationExceptionif an observer tries to unsubscribe itself inside its ownupdate()callback. - Smart Reference Traversal: In the C++ example, storing observers as
std::weak_ptrallows them to be garbage collected automatically when their scope ends, avoiding memory leaks.
11. Execution Flow
- Subscription: The client registers observers with the subject using
subscribe(). - State Change: The subject updates its state (e.g.
setStockUpdate()). - Notification: The subject iterates over its observer list and invokes
update()on each. - Execution: Concrete observers execute their custom update logic (e.g., triggering alerts, refreshing screens).
12. Internal Working (The Lapsed Listener Memory Leak)
Failing to manage references in the Observer pattern can cause severe memory leaks:
- Lapsed Listener Leak: When a short-lived observer subscribes to a long-lived subject, the subject holds a strong reference to the observer. If the observer is discarded without calling
unsubscribe(), the subject's reference keeps the observer alive on the heap, preventing garbage collection. - Prevention using Weak References: To prevent this issue, store observers using weak references (e.g.,
std::weak_ptrin C++ orWeakHashMapin Java). This allows the garbage collector to reclaim observers when they are no longer referenced elsewhere.
13. Complexity Analysis
- Time Complexity: $O(1)$ to subscribe/unsubscribe. Broadcast notifications take $O(K)$ where $K$ is the number of subscribed observers.
- Space Complexity: $O(K)$ to store observer references inside the subject list.
14. Best Practices
- Use Thread-Safe Collections: Always protect the observer list with thread-safe collections (e.g.
CopyOnWriteArrayList) to prevent race conditions during updates. - Keep Callbacks Fast: Avoid running blocking operations (e.g., file writes, network calls) inside the
update()thread. Instead, offload these tasks to background event queues.
15. Common Mistakes
- Lapsed Listeners: Forgetting to unsubscribe observers when their lifecycle ends, causing memory leaks.
- Recursive Update Loops: Implementing an
update()method that triggers another update on the subject, causing infinite loops and stack overflows. - Synchronous Blocking Callbacks: Running slow, blocking calls inside the callback thread, stalling updates for all other observers.
16. Framework Usage
- Java's PropertyChangeListener: Standard JavaBeans API uses
PropertyChangeListenerandPropertyChangeSupportto implement the Observer pattern. - Spring @EventListener: Spring allows components to listen for application events simply by annotating methods with
@EventListener. - Reactive Programming (RxJava/WebSockets): Modern reactive streams use Observer patterns to push data notifications to subscriber pipelines.
17. Interview Discussion
Answer: - Observer: The subject maintains a direct list of observers and notifies them directly. - Publish-Subscribe: Introduces a broker/event channel between publishers and subscribers, meaning publishers and subscribers have no direct knowledge of each other.
Answer: Notify observers asynchronously by pushing event objects to thread pools or message queues, avoiding locking the main publisher thread during callback execution.
Answer: It is a memory leak caused when a subject retains a strong reference to a discarded observer, preventing GC. It is resolved by storing observers using weak references (
std::weak_ptr or WeakReference).
18. Practice Exercises
- Easy: Write a Python program containing a custom Observer pattern notifying subscribers of new emails.
- Medium: Design a
WeatherStationsubject that pushes weather metrics (temp, wind speed) to different dashboard observers using the Pull model. - Hard: Build a thread-safe asynchronous event dispatcher that processes notifications using an ExecutorService pool.
19. Challenge Problem
Design a Real-Time Multiplayer Game Lobby Matchmaker. A central GameLobby subject tracks changes in match lobbies (lobby capacity updates, game status changes, player exits). Multiple client systems (chat service, leaderboard service, user interface, analytics engine) must monitor these lobby state changes. If the chat service crashes, the game lobby must continue notifying other services without stalling. Implement this matchmaker in Java, Python, or C++, and verify execution with dynamic registrations and unsubscriptions.
20. Summary & Cheat Sheet
- Observer decouples event publishers (Subjects) from subscribers (Observers).
- Use copy-on-write lists to support safe unsubscriptions during broadcasts.
- Always use weak references to prevent Lapsed Listener memory leaks.
- Keep callback methods fast and run blocking calls asynchronously.
21. Quiz
1. What is the primary purpose of the Observer design pattern?
A) To simplify complex subsystems
B) To establish a one-to-many relationship where state changes trigger automatic notifications (Correct)
C) To encapsulate interchangeable algorithms
2. What is the Lapsed Listener Problem?
A) A thread deadlock in message queues
B) A memory leak caused when a subject retains strong references to discarded observers (Correct)
C) A compiler optimization warning
3. How do you prevent Lapsed Listener memory leaks in garbage-collected environments?
A) Make the observers final classes
B) Use weak references (WeakReference or std::weak_ptr) to store observers (Correct)
C) Force observers to run on single threads
4. In the Pull notification model, what does the subject pass during updates?
A) All private variables as arguments
B) Minimal data or a reference to itself, letting observers query needed states (Correct)
C) A copy of the database connection
5. What exception is risked in Java when removing a subscriber during an active broadcast loop on a standard ArrayList?
A) NullPointerException
B) ConcurrentModificationException (Correct)
C) StackOverflowError
6. How does the Publish-Subscribe pattern differ from the Observer pattern?
A) Publish-Subscribe uses multiple inheritance; Observer does not
B) Publish-Subscribe introduces an event broker between publishers and subscribers, removing direct coupling (Correct)
C) Observer pattern does not compile in C++
7. What is the best way to handle slow, blocking tasks inside an observer's update() method?
A) Execute them synchronously in the main thread
B) Offload execution asynchronously to background threads or event queues (Correct)
C) Throw an exception to stop broadcast
8. Can an object be both a Subject and an Observer at the same time?
A) No, to maintain separation of concerns
B) Yes, by implementing both interfaces to act as an intermediate node in notification pipelines (Correct)
C) Only when using database connection pools
9. In C++, why is std::weak_ptr preferred over std::shared_ptr to store observer references inside subjects?
A) To save virtual table lookups
B) To prevent circular reference dependencies that prevent objects from being deleted (Correct)
C) To enforce thread-safety
10. Does the Observer pattern support the Open/Closed Principle?
A) Yes, because you can introduce new observer classes without modifying the subject code (Correct)
B) No, because adding subscribers forces rewriting notification loops
C) Only when using Spring framework event listeners
22. Next Lesson Preview
In the next lesson, we will explore the Command Pattern. We will learn how to encapsulate requests as objects, allowing you to parameterize clients, queue operations, and support undoable tasks!
Related Topics
- StrategyDefine a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients.
- IteratorAccess elements of an aggregate object sequentially without exposing its underlying representation (list, stack, tree, graph).
- CommandEncapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.