12 The Observer Design Pattern
This chapter explains the Observer design pattern through the familiar publisher-subscriber model, highlighting how publishers broadcast state changes to many independent subscribers without knowing their implementations. It emphasizes desired qualities such as immediate notifications, optional data retrieval by subscribers, dynamic subscribe/unsubscribe at runtime, and loose coupling between components. Framed as producer-consumer and subject-observer, the pattern enables flexible, extensible systems where each observer can react to the same event stream in different ways.
The running example tracks a college baseball game’s hit events to generate three reports: a live log, a per-player table, and a postgame bar chart. An initial, pre-pattern version works but hardcodes subscribers and method calls, tightly coupling the publisher to specific report classes and their method names. This makes adding or removing reports brittle and violates the goal of loosely coupled, easily extensible components.
Refactoring with Observer introduces a Subject that manages an observers list with attach/detach and notifies all via update, while observers decide whether to pull the current event. The publisher stores each new event and notifies observers with the player’s name; observers like the logger, table, and graph fetch and process the current event as needed, and an additional fan-club observer demonstrates selective interest and detachment after its first match. The result is less hardcoding, greater flexibility, looser coupling (coding to an interface), and clearer responsibilities—publishers produce and announce data; observers independently decide what to do with it, supporting principles such as Single Responsibility, Encapsulate What Varies, and Least Knowledge.
An example of the publisher-subscriber model. The publisher component produces hit events that are consumed as they occur by the subscriber components to generate the various reports.
An example graph report showing a bar chart.
The initial architecture of the baseball game reports application. Method report_hits() of class BaseballReporter repeatedly calls the next_event() method of class EventMaker to obtain new Event objects one at a time. It passes each Event object to the log_event(), handle_event(), and receive_event() methods of the report generation classes. Each report generation class can process the Event object in its own way.
This version of the application is modeled from the Observer Design Pattern. The content publisher BaseballReporter is a subclass of Subject, and the report generation classes each implements interface Observer. The Subject superclass’s private instance variable _observers is a list of subscribed observers. The grayed-out portions of the diagram have not changed logically from figure 12.3.
The generic model of the Observer Design Pattern. Compare with figure 12.3. During run time, we can add subscriber Observer objects to or remove them from the publisher Subject class’s collection of Observer objects. Whenever an ExemplarSubject changes state, the Subject class’s notify() method iterates over the observers collection and calls method update() of each ConcreteObserver object. Each ConcreteObject can then choose to call the get_state() method of the ExemplarSubject class to get the new state.
Summary
- The Observer Design Pattern implements the publisher-subscriber model by defining a one-to-many dependency between a subject object (the publisher) and a set of observer objects (the subscribers) so that when the subject object changes state, all the observer objects are notified automatically.
- Once notified, an observer can choose whether or not to fetch the contents of the new state.
- The publisher is loosely coupled with the subscribers, and the publisher doesn’t know or care what the subscribers do with the state changes.
- We can add or remove subscribers without changing the publisher’s code.
FAQ
What is the Observer Design Pattern, and how is it used in this chapter’s example?
The Observer pattern defines a one-to-many dependency so when a subject changes state, all subscribed observers are notified. In the example, BaseballReporter is the Subject (publisher) that produces Event objects (hits/outs), and LogReport, TableReport, and GraphReport are Observers (subscribers) that react to those events in their own ways.How does the publisher-subscriber model work in the baseball game scenario?
EventMaker produces Event objects. BaseballReporter obtains the next Event, stores it as its current_event, and notifies all subscribed observers. Each observer decides how to respond—some act immediately, others accumulate data for a final report.What design problems existed before applying Observer?
Subscribers were hardcoded into BaseballReporter, and the publisher needed to know each subscriber’s concrete API (e.g., log_event, receive_event, handle_event). This tight coupling made it difficult to add/remove subscribers or change their implementations without modifying the publisher.What are the key roles and responsibilities of Subject and Observer?
Subject maintains a list of observers and provides attach/detach to manage subscriptions and notify to broadcast changes. Observer defines an update method that Subjects call on state change. Concrete observers implement update to process the subject’s state in their own way.How do attach() and detach() enable dynamic subscriptions?
Observers register with Subject via attach(self) and can unregister via detach(self). Subject keeps a list (_observers) and iterates it during notifications. This allows adding/removing observers at runtime without changing publisher code, as demonstrated by FanClubReport detaching after it gets its needed data.Why does update() receive a player name instead of the full Event, and how do observers get the content?
Passing a lightweight hint (player_name) lets observers decide whether to “pull” the full content via reporter.current_event. This selective pull avoids unnecessary data transfer and processing, which is useful when content is large or observers only care about certain events.How do the three reports (log, table, graph) process the same events differently?
LogReport prints a line immediately for each event; TableReport aggregates per-player counts of outs and hit types and prints a table after the game; GraphReport aggregates counts of hit types across all players and renders a bar chart after the game.How is the end of the game signaled to observers?
BaseballReporter calls notify with None (a sentinel) when the stream ends. Observers interpret None as “finalize now,” for example TableReport prints the table and GraphReport displays the chart.What benefits does using Observer bring to this design?
- Less hardcoding: subscribers are not baked into the publisher.- Flexibility/extensibility: add or remove observers at runtime; create new observers without modifying the publisher.
- Loose coupling: the publisher only depends on the Observer interface (update), not concrete implementations.
- Single Responsibility: publisher only produces and announces events; observers decide how to use them. These align with the design features DF1–DF7 outlined in the chapter.
Software Design for Python Programmers ebook for free