Skip to content

๐Ÿ“‹ 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() and next() 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

  1. (Functional): Traverse both List based menus and Dictionary based menus using the same client code.
  2. (Technical): The traversal logic must be encapsulated in separate Iterator classes.
  3. (Technical): The Client (Waitress) must rely only on the Iterator interface.

๐Ÿ—๏ธ 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 new TreeIterator without 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

  1. Unit Test Iterators: Create a list, wrap it in an iterator, and verify next() returns items in order and has_next() returns false at the end.
  2. 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.
  • Composite โ€” Iterator is used to traverse Composite trees.
  • Factory Method โ€” create_iterator() is a Factory Method.
  • Memento โ€” Can capture the state of an iteration.