๐ Iterator Pattern: Unified Menu Traversal
๐ Overview
The Iterator Pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation (list, stack, tree, etc.). It decouples the traversal algorithms from the data structure, allowing a client to iterate over diverse collections using a uniform interface.
Core Concepts
- Uniform Interface:
hasNext()andnext()work the same way whether you're traversing a linked list or a hash map. - Single Responsibility: The collection focuses on storage; the iterator focuses on traversal.
- Encapsulation: The client doesn't know (or care) how the data is stored internally.
๐ญ The Engineering Story & Problem
๐ก The Villain (The Problem)
Meet "The Frustrated Waitress." She works for a company that just merged two restaurants: "Objectville Diner" and "Pancake House."
- Pancake House stores their menu items in a Python List ([]).
- Objectville Diner stores their menu items in a Python Dictionary ({}).
To print the combined menu, the Waitress has to write two completely different loops. One uses an index for i in range(len(list)) and the other iterates over keys for key in dict. If a third restaurant joins (using a Tree or Set), she has to write another specific loop. Her code is tightly coupled to the internal data structures of every restaurant.
๐ฆธ The Hero (The Solution)
The Iterator Pattern introduces a standard "Remote Control" for traversal. We define an Iterator interface with just two methods: has_next() and next().
Each restaurant implements its own create_iterator() method.
- Pancake House returns a ListIterator.
- Diner returns a DictIterator.
The Waitress now just asks for an iterator. She runs a simple while iterator.has_next(): print(iterator.next()). She doesn't know if it's a list, an array, or a database cursor.
๐ Requirements & Constraints
- (Functional): Traverse both
Listbased menus andDictionarybased menus using the same client code. - (Technical): The traversal logic must be encapsulated in separate Iterator classes.
- (Technical): The Client (Waitress) must rely only on the
Iteratorinterface.
๐๏ธ Structure & Blueprint
Class Diagram
classDiagram
direction TB
class Iterator {
<<interface>>
+has_next() bool
+next() Item
}
class Menu {
<<interface>>
+create_iterator() Iterator
}
class PancakeMenu {
-menu_items: List
+create_iterator() Iterator
}
class DinerMenu {
-menu_items: Dict
+create_iterator() Iterator
}
class PancakeIterator {
+has_next()
+next()
}
class DinerIterator {
+has_next()
+next()
}
Iterator <|.. PancakeIterator
Iterator <|.. DinerIterator
Menu <|.. PancakeMenu
Menu <|.. DinerMenu
PancakeMenu ..> PancakeIterator : Creates
DinerMenu ..> DinerIterator : Creates
Runtime Context (Sequence)
sequenceDiagram
participant Client
participant Menu
participant Iterator
Client->>Menu: create_iterator()
Menu-->>Client: Iterator
loop While has_next()
Client->>Iterator: has_next()
Iterator-->>Client: true
Client->>Iterator: next()
Iterator-->>Client: MenuItem
end
๐ป Implementation & Code
๐ง SOLID Principles Applied
- Single Responsibility: The Menu classes manage data; the Iterator classes manage traversal.
- Open/Closed: You can add a new
CafeMenu(using a Tree) with a newTreeIteratorwithout changing the Waitress code.
๐ The Code
The Villain's Code (Without Pattern)
class Waitress:
def print_menu(self):
# ๐ก Tightly coupled to implementation details
pancake_menu = PancakeHouseMenu()
diner_menu = DinerMenu()
# Loop 1: List
for item in pancake_menu.get_menu_items():
print(item)
# Loop 2: Dictionary
menu_dict = diner_menu.get_menu_items()
for key in menu_dict:
print(menu_dict[key])
# If we add a 3rd menu, we need a 3rd loop type!
The Hero's Code (With Pattern)
class Dinner:
def __init__(self) -> None:
self.items : list[str] = []
def get_items(self) -> list[str]:
return self.items
def add_item(self, item: str) -> None:
self.items.append(item)
def remove_item(self, item: str) -> None:
if item in self.items:
self.items.remove(item)
class Pancake:
def __init__(self) -> None:
self.dishes : dict[str, int] = {}
def get_dishes(self) -> dict[str, int]:
return self.dishes
def add_dish(self, dish: str, price: int) -> None:
self.dishes[dish] = price
def delete_dish(self, dish: str) -> None:
self.dishes.pop(dish, None)
class Merger:
def __init__(self, dinner: Dinner, pancake: Pancake) -> None:
self.dinner : Dinner = dinner
self.item_count = dinner.items.__sizeof__()
self.pancake : Pancake = pancake
self.dish_count = pancake.dishes.__sizeof__()
self.count : int = 0
self.iter = iter(self.dinner.get_items())
def has_next(self) -> bool:
if self.count > self.item_count + self.dish_count:
return False
return True
def next(self):
self.count = self.count + 1
try:
next(self.iter)
except:
self.iter = iter(self.pancake.get_dishes())
class Waitress:
def __init__(self, merger: Merger) -> None:
self.merger : Merger = merger
def menu(self) -> None:
while self.merger.has_next():
self.merger.next()
# Created Dinner object and add items
dinner = Dinner()
dinner.add_item("Grilled Chicken")
dinner.add_item("Caesar Salad")
dinner.add_item("Garlic Bread")
dinner.add_item("Chocolate Cake")
# Created Pancake object and add dishes with prices
pancake_menu = Pancake()
pancake_menu.add_dish("Classic Pancake", 5)
pancake_menu.add_dish("Blueberry Pancake", 7)
pancake_menu.add_dish("Chocolate Chip Pancake", 8)
pancake_menu.add_dish("Banana Walnut Pancake", 9)
merger = Merger(dinner=dinner, pancake=pancake_menu)
waitress = Waitress(merger=merger)
waitress.menu()
โ๏ธ Trade-offs & Testing
| Pros (Why it works) | Cons (The Twist / Pitfalls) |
|---|---|
| Uniformity: Client code looks the same for all collections. | Overhead: Creating an object for simple iteration is slower than a raw for loop. |
| Encapsulation: Hides internal data structures (Arrays, Lists, Trees). | Complexity: Needs a new class for every type of collection. |
| Flexibility: Can support multiple simultaneous traversals. | Modification: Iterating while modifying the collection (add/remove) is dangerous. |
๐งช Testing Strategy
- Unit Test Iterators: Create a list, wrap it in an iterator, and verify
next()returns items in order andhas_next()returns false at the end. - Test Empty Collections: Ensure the iterator handles empty lists gracefully without crashing.
๐ค Interview Toolkit
- Interview Signal: Mastery of abstraction layers and interface-based design.
- When to Use:
- "Traverse a composite structure (like a tree)..."
- "Hide the complexity of a data structure..."
- "Write a generic algorithm that works on any collection..."
- Scalability Probe: "How to handle a database result set with 1 million rows?" (Answer: Use a "Pagination Iterator" or a "Cursor Iterator" that lazy-loads data in chunks.)
- Design Alternatives:
- Visitor: Often used with Iterator to perform an operation on each element.
- Composite: Iterator is the standard way to traverse a Composite tree.
๐ Related Patterns
- Composite โ Iterator is used to traverse Composite trees.
- Factory Method โ
create_iterator()is a Factory Method. - Memento โ Can capture the state of an iteration.