11 The Iterator and Visitor Design Patterns
This chapter explains two complementary design patterns that help decouple algorithms from data structures so software can evolve with less friction. It uses sports-report examples to show how to traverse different containers uniformly and how to run multiple computations over the same hierarchical data. The Iterator Design Pattern lets one algorithm work across varied sequential collections without knowing their internal representation, while the Visitor Design Pattern lets multiple algorithms operate over a single object structure—often a tree—whose nodes may be of different types.
For Iterator, the chapter starts with a brittle approach in which a reporting class contains separate loops for lists, generators, and dictionaries, leading to duplication and tight coupling. Refactoring introduces an Iterator interface (with next and has_next) and a concrete iterator per collection type; each Team exposes an iterator property, and the reporting class uses one uniform loop. This delegates traversal to the iterators, hides each collection’s representation, reduces duplication, and makes it easy to add new collection types. The text also notes that although Python’s built-in iterators are convenient, wrapping them behind a common interface enables polymorphic selection and looser coupling aligned with the pattern.
For Visitor, the chapter begins with a tree of intramural sports data where node classes (Intramural, Sport, Game, Hall) contain both structure and reporting logic, violating single responsibility and making change costly. The refactoring introduces a Node interface with accept(visitor), a Visitor interface with visit methods for each node type, and concrete visitors for the Scores, Activities, and Winnings reports. Each visitor encapsulates a reporting algorithm and is applied in separate passes by calling accept on the root, while nodes expose only the minimal data needed via properties. This yields encapsulated algorithms, loose coupling between data and behavior, and easy extensibility. The guidance: choose Iterator when one algorithm must traverse many kinds of sequential collections; choose Visitor when many algorithms must operate over one heterogeneous object structure.
Each Team subclass stores its Player objects in a different type of sequential data collection: list, generator, or dictionary. Therefore, class TeamReport has three printing methods, one for each type of collection.
A single algorithm can operate on different sequential data collections.
Each Team subclass is paired with an Iterator class that is appropriate for its sequential collection of Player objects. Class TeamReport delegates iterating over the various types of sequential collections to the Iterator classes ListIterator, GeneratorIterator, and DictionaryIterator. The grayed-out portions of the diagram haven’t changed logically from figure 11.1.
The generic model of the Iterator Design Pattern. Compare with figure 11.3. The client delegates iterating over the sequential collections to the Iterator subclasses, and thus the client is loosely coupled from the collections. Table 11.1 shows how the example application applies the pattern.
An example tree data structure of hierarchical data for a store. Store is the root node. The product nodes at the bottom (shaded in gray) are the leaf nodes.
A data collection as a hierarchical tree structure that represents intramural baseball, football, and volleyball intramural games played by teams from residence halls. The Intramural node is the root node, and it also has a separate list of residence hall objects. The residence hall objects at the bottom (shaded in gray) representing the winner and loser of each game are the tree’s leaf nodes.
The four major classes of the first version of the intramural sports reports application. To implement the hierarchical tree structure, each class except Hall aggregates the class that is at the next lower level in the hierarchy. The Intramural class’s list of Hall objects is not part of the hierarchical tree structure.
A call chain through the methods of the main classes is necessary to generate each report.
Different algorithms can operate on a single data collection.
The parts that haven’t changed logically from figure 11.7 are shown in gray. Now each of the tree node classes Intramural, Sport, Game, and Hall implements interface Node. The three visitor classes ScoresReportVisitor, ActivitiesReportVisitor, and WinningsReportVisitor each implements interface Visitor and encapsulates a report generation algorithm.
The generic model of the Visitor Design Pattern. Compare with figure 11.10. Each Visitor class encapsulates an algorithm. To execute an algorithm during a pass of the Element objects, each element object accepts a Visitor object. The accept() function of each concrete Element object calls the visit method that corresponds to that Element object. The visit methods together execute the Visitor class’s algorithm as they visit the Element objects one after another.
Summary
- The Iterator Design Pattern models a solution for a software architecture that has a single algorithm that processes elements one after another stored in multiple sequential collections, without exposing the how the collections are implemented. The client and its algorithm are loosely coupled with the collections.
- We will be able to modify or delete a data collection, or add a new sequential collection with its iterator, without modifying the algorithm or the existing collections.
- The Visitor Design Pattern models a solution for a software architecture that makes multiple passes over the objects of a single data collection. The objects can be instantiated from diverse classes. Each pass executes a different algorithm on the objects, and each algorithm has methods to process each type of object.
- The Visitor Design Pattern encapsulates each algorithm as a visitor class so the algorithms are loosely coupled with the data collection, and the algorithms are loosely coupled with each other. We will be able to modify or delete algorithms or add new algorithms without modifying the data collection or the existing algorithms.
FAQ
What problem does the Iterator Design Pattern solve in the teams example?
It lets one printing algorithm work over different sequential collections (list, generator, dictionary) without knowing how players are stored. Each collection exposes an iterator object that knows how to traverse its data and yield Player objects.How does refactoring to use iterators reduce coupling and duplication?
TeamReport has a single print(team) method that asks team.iterator for an iterator, then loops using has_next()/next(). Each Team subclass returns an appropriate iterator, so TeamReport doesn’t depend on collection details. Iteration logic lives in ListIterator, GeneratorIterator, and DictionaryIterator, eliminating duplicated code paths.Why not just use Python’s built-in iterators?
Built-in iterators don’t share a common interface you can target with polymorphism, so you can’t “code to an interface” and swap iterator types at runtime. They also expose the underlying representation. You can wrap them (as in GeneratorIterator) behind a common Iterator interface to regain polymorphism and encapsulation.What are the main roles in the Iterator pattern as applied here?
- Client: TeamReport (runs the algorithm)- Sequential collection superclass: Team
- Concrete collections: Team_1, Team_2, Team_3
- Iterator interface: Iterator with next() and has_next()
- Concrete iterators: ListIterator, GeneratorIterator, DictionaryIterator
- Item iterated: Player
What makes a collection “sequential” in this chapter’s terms?
It has a first and last element, each non-last element has a unique next element, you can access items one-by-one from first to last, and you can detect when the end is reached. Lists, tuples, generators, and even dictionaries (via a key sequence) qualify.What problem does the Visitor Design Pattern solve in the intramural example?
It separates multiple algorithms (three different reports) from a single heterogeneous, tree-structured data collection (Intramural → Sports → Games → Halls). Each report is encapsulated in its own Visitor, avoiding mixing traversal/report code inside the node classes.How do accept() and visit() work together (double dispatch)?
Each node class implements accept(visitor) and calls the corresponding visitor method (e.g., visitor.visit_sport(self)). The visitor receives the node, uses its public API, and the node forwards the visitor to its children by calling accept on them. This binds the algorithm to the node type at runtime without exposing structure details.What benefits were gained by applying the Visitor pattern?
- Algorithms are encapsulated in separate Visitor classes (easy to change or add)- Node classes have a single responsibility: maintain the tree and expose a public API
- Loose coupling between data structure and algorithms, and among the algorithms themselves
Software Design for Python Programmers ebook for free