Iterator Pattern

Issue

  • I want to iterate through a collection, but I don’t want to care about its structure

  • Known iterable types are not sufficient or I don’t know about the structure, just need a way to get all elements from the structure

  • The structure must not be of any known iterator type (e.g. list), but can be anything I want

  • Trying to reshape the data of differently structured classes to become compatible with each other might be difficult to achieve and the logic to get to this format will be different for each structure

Solution

  • Uniform way to iterate over various collections of items, which derive from the same interface, but vary in its structure

  • The iterate method is not returning all elements as flattened list at once, but returns item after item with each time the iterate method is called

  • That way the collection does not expose it’s structure

  • Lazy Evaluation: By return only the next item, a result can be immediately checked and stopped if wanted item is returned. This allows infinite collections

  • the iterator knows its current item, as well as the last and next one

  • The Iterator Pattern is a way of encapsulating the iteration specifics, to move it out of the code using it

Definition

The Iterator Pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

        classDiagram
    Iterable <|-- ConcreteIterable : implements
    Iterator <|-- ConcreteIterator : implements
    Iterable <-- Client : HAS-A
    Iterator <-- Client : HAS-A
    ConcreteIterator <-- ConcreteIterable : creates
    ConcreteIterable <-- ConcreteIterator : HAS-A
    class Iterable {
        +get_iterator() Iterator
    }
    class ConcreteIterable {
        +get_iterator() Iterator
    }
    class Iterator {
        +has_next() bool
        +next()
        +current() Item
    }
    class ConcreteIterator {
        +has_next() bool
        +next()
        +current() Item
    }
    
  • the get_iterator() method will return an object of type Iterator

  • Each ConcreteIterable may have a different structure, so get_iterator() will return a different ConcreteIterator

  • a ConcreteIterable is a factory, that produces ConcreteIterators

  • the has_next() method returns, if the iterator has a next element (or is at its end)

  • the next() method moves the pointer to the next element

  • the current() method returns the element the iterator’s index is currently pointing to (here: using the type Item)

  • the has_next(), next() and current() methods can be combined (either all of them or just two), but this violates the Command-query separation principle (see below)

Command-Query separation

Functions should either be queries or commands. Queries are for getting data, but must not change the state. Commands are supposed to change the state, but must not return any data. Functions must not do both.

  • ConcreteIterable create ConcreteIterator instances, not Iterator instances

  • The HAS-A relationship between ConcreteIterator and ConcreteIterable is optional. In case it exists, the ConcreteIterable must pass itself into the ConcreteIterator constructor, so the ConcreteIterator has a reference to what it is supposed to iterate over. The other way is, not to pass the ConcreteIterable into the constructor, but only the data, the ConcreteIterator is supposed to iterate over

The reason why to keep Iterable and Iterator separate and not combine them in one class, is that it had more than one responsibility: defining how to iterate over a set of item (Iterable) and performing iteration over an iterable (Iterator). This violates the Single-responsibility principle:

Single-Responsibility Principle

A class should have only one reason to change

This implies a class must have only one purpose. For instances, printing a set of data. In that case, the only reason to change the class is because the way the data is printed should be different.

Example

Find a template example for Python at https://refactoring.guru/design-patterns/iterator/python/example

from abc import ABC, abstractmethod


class Item(ABC):
    name = None


class Sword(Item):

    def __init__(self):
        self.name = "Sword"


class Shield(Item):

    def __init__(self):
        self.name = "Shield"


# Iterator
class InventoryIterator(ABC):

    @abstractmethod
    def is_done(self) -> bool:
        pass

    @abstractmethod
    def next(self) -> None:
        pass

    @abstractmethod
    def current(self) -> Item:
        pass


# Iterable
class Inventory(ABC):

    @abstractmethod
    def get_iterator(self) -> InventoryIterator:
        pass


# ConcreteIterable 1
class HandHeldInventory(Inventory):

    def __init__(self, right: Item, left: Item):
        self._right = right
        self._left = left

    def get_iterator(self) -> InventoryIterator:
        return HandHeldInventoryIterator(self)

    @property
    def right(self) -> Item:
        return self._right

    @right.setter
    def right(self, item: Item) -> None:
        self._right = item

    @property
    def left(self) -> Item:
        return self._left

    @left.setter
    def left(self, item: Item) -> None:
        self._left = item


# ConcreteIterable 2
class BackPackInventory(Inventory):

    def get_iterator(self) -> InventoryIterator:
        pass

    # implementation ...

    # backpack can contain much more items than HandHeldInventory,
    # so handling both via Iterator Pattern makes sense, even though
    # HandHeldInventory can only hold two items


# ConcreteIterator 1
class HandHeldInventoryIterator(InventoryIterator):

    # iterate oder: -> right -> left

    def __init__(self, inventory: HandHeldInventory):
        self.inventory = inventory
        self.index = 0

    def next(self) -> None:
        self.index += 1

    def current(self) -> Item:
        if self.index == 0:
            return self.inventory.right
        if self.index == 1:
            return self.inventory.left
        else:
            IndexError(f"HandHeldInventory cannot have index of {self.index}")

    def is_done(self) -> bool:
        return self.index > 1


if __name__ == "__main__":

    sword = Sword()
    shield = Shield()
    inventory = HandHeldInventory(right=sword, left=shield)
    iterator = inventory.get_iterator()

    while not iterator.is_done():
        print(iterator.current().name)
        # do other cool stuff with current item
        iterator.next()