The Iterator design pattern is a powerful tool in software development, providing a structured way to traverse collections without exposing their internal workings. Whether you're working with lists, trees, or custom data structures, this pattern ensures clean and efficient navigation, promoting code that is both readable and maintainable. This blog explores the journey from a naive solution to a refined implementation of the Iterator pattern.
Background
Imagine you have a collection of shapes (e.g., circles, squares) and need to iterate through them. Initially, you might hardcode the iteration logic within the client code.
class Shape:
def __init__(self, name: str):
self.name = name
shapes = [Shape("Circle"), Shape("Square"), Shape("Triangle")]
# Naive iteration logic in the client
for i in range(len(shapes)):
print(shapes[i].name)
Problems
While this works for small cases, it quickly becomes unmanageable as your application grows.
- Tight Coupling: The client is tightly bound to the collection's structure.
- Single Responsibility Principle Violation: The client handles both business logic and iteration.
- Extensibility Issues: Changing the collection type requires rewriting the iteration logic. E.g. you might want to change list to dictionary. This change will be ripple effect for all the components using the previous data type.
Incremental Refinement
The key to solving the problem is externalizing the iteration logic. Let's refine the naive solution step by step.
Step 1: External Iterator Class
Move the iteration logic into a separate class.
from typing import List
class ShapeIterator:
def __init__(self, shapes: List[Shape]):
self.shapes = shapes
self.index = 0
def has_next(self) -> bool:
return self.index < len(self.shapes)
def next(self) -> Shape:
shape = self.shapes[self.index]
self.index += 1
return shape
shapes = [Shape("Circle"), Shape("Square"), Shape("Triangle")]
iterator = ShapeIterator(shapes)
while iterator.has_next():
print(iterator.next().name)
What do we have achieved? - The client is no longer concerned with the collection's structure.
Step 2: Implementing an Iterable Interface
Standardize iteration by introducing an interface for collections.
class Iterable:
def __iter__(self):
raise NotImplementedError
class ShapeCollection(Iterable):
def __init__(self):
self.shapes = []
def add_shape(self, shape: Shape):
self.shapes.append(shape)
def __iter__(self):
return ShapeIterator(self.shapes)
shapes = ShapeCollection()
shapes.add_shape(Shape("Circle"))
shapes.add_shape(Shape("Square"))
shapes.add_shape(Shape("Triangle"))
for shape in shapes:
print(shape.name)
Benefits:
- Single Responsibility Principle: Separation of concerns between client, collection, and iterator.
- Open/Closed Principle: Adding new collection types doesn't affect existing logic.
Key Takeaways
The Iterator design pattern is more than just a way to traverse collections - it's a tool for writing clean, extensible, and maintainable code. By decoupling iteration logic from collections, we achieve better separation of concerns and adherence to design principles like SRP and OCP. Python's built-in mechanisms, such as iter and next, make it easy to implement this pattern elegantly. Embrace this approach in your projects to simplify traversal while keeping your codebase flexible and future-proof.
I hope you have learned something. If you found it valuable, hit the heart button ❤️ and consider following me for more such content.
Top comments (0)