The iterator pattern is used to access the elements of a collection object sequentially without exposing its underlying representation. It provides a way to traverse a collection without needing to know the internal details of how the collection is structured.
Applicability
- When you want to provide a standard way to iterate through different types of collection (e.g. trees, lists, maps, etc.) without exposing their internal structure.
- When you want to support multiple traversal algorithms over the same collection
Approach
- Define an iterator interface that declares methods like
hasNext()
andnext()
to access elements sequentially - Create concrete implementations of the iterator that know how to travers a specific collection
- Define an aggregate (or iterable) interface that declares a method like
createIterator()
to return an iterator - Implement the aggregate interface in the concrete collection class, which returns a suitable iterator interface
--- title: Iterator Design Pattern --- classDiagram direction TB class Client Client --> Aggregate Client --> Iterator class Aggregate { <<interface>> +createIterator() } class CAggregate{ +createIterator() } Aggregate<|.. CAggregate class Iterator { <<interface>> +hasNext() +next() } class CIterator{ -addedState +hasNext() +next() } Iterator<|.. CIterator CAggregate <-- CIterator CAggregate ..> CIterator
Componentes
- Iterator
- An interface that defines how elements can be accessed and iterated
- Concrete iterator
- Implements the iterator interface, keeping track of the current position in the traversal
- Agregate (iterable)
- An interface that declares the method for creating an iterator
- Concrete aggregate
- Implements the aggregate interface and returns a new iterator instance that can traverse its elements
Example (Java)
Identify iterator pattern components
Aggregate: Container
Concrete Aggregate: NameRepository
Iterator: Iterator
(abstract)
Concrete Iterator: NameIterator
Aggregate (Iterable)
The Container
Interface declares a method to return the iterator.
interface Container {
Iterator getIterator();
}
Concrete aggregate
NameRepository
implements the Container
interface returns a NameIterator
.
Support multiple iterator algorithms
A concrete aggregate can have many iterator algorithms, this is one of the strengths of the iterator pattern. It may require different traversal methods or ways to iterate over the same data structure. Such as forward or backward, in-order or pre-order (for tree structures), etc.
class NameRepository implements Container {
private String[] names = { "Alice", "Bob", "Charlie", "Diana" };
public enum IterationType {
FORWARD, REVERSE, EVEN_INDEX
}
public Iterator getIterator(IterationType type) {
switch (type) {
case FORWARD: return new ForwardIterator(names);
case REVERSE: return new ReverseIterator(names);
case EVEN_INDEX: return new EvenIndexIterator(names);
default: return new ForwardIterator(names);
}
private Iterator getForwardIterator() {
return new ForwardIterator(names);
}
private Iterator getReverseIterator() {
return new ReverseIterator(names);
}
private Iterator getEvenIndexIterator() {
return new EvenIndexIterator(names);
}
}
Iterator
This interface defines the standard operations for traversing a collection
interface Iterator {
boolean hasNext();
Object next();
}
Concrete iterator
The name repository supports different traversal orders and logics.
class ForwardIterator implements Iterator {
private String[] names;
private int index = 0;
public ForwardIterator(String[] names) {
this.names = names;
}
public boolean hasNext() {
return index < names.length;
}
public Object next() {
return hasNext() ? names[index++] : null;
}
}
class ReverseIterator implements Iterator {
private String[] names;
private int index;
public ReverseIterator(String[] names) {
this.names = names;
this.index = names.length - 1;
}
public boolean hasNext() {
return index >= 0;
}
public Object next() {
return hasNext() ? names[index--] : null;
}
}
class EvenIndexIterator implements Iterator {
private String[] names;
private int index = 0;
public EvenIndexIterator(String[] names) {
this.names = names;
}
public boolean hasNext() {
return index < names.length;
}
public Object next() {
Object value = names[index];
index += 2;
return value;
}
}
Application code
public class Demo {
public static void main(String[] args) {
NameRepository namesRepo = new NameRepository();
System.out.println("=== Forward Iterator ===");
Iterator forward = namesRepo.getForwardIterator();
while (forward.hasNext()) {
System.out.println("Name: " + forward.next());
}
System.out.println("\n=== Reverse Iterator ===");
Iterator reverse = namesRepo.getReverseIterator();
while (reverse.hasNext()) {
System.out.println("Name: " + reverse.next());
}
System.out.println("\n=== Even Index Iterator ===");
Iterator evenIndex = namesRepo.getEvenIndexIterator();
while (evenIndex.hasNext()) {
System.out.println("Name: " + evenIndex.next());
}
}
}
Example (Python)
The iterator pattern is built in to Python language. To make a class iterable, you need to implement the iterator protocol, which involves two dunder methods:
__iter__(self)
: Returns the iterator object__next__(self)
: Returns the next value, or risesStopIteration
when done
class NameCollection:
def __init__(self, names):
self.names = names
def __iter__(self):
return NameIterator(self.names)
class NameIterator:
def __init__(self, names):
self.names = names
self.index = 0
def __next__(self):
if self.index >= len(self.names):
raise StopIteration
result = self.names[self.index]
self.index += 1
return result
def __iter__(self):
return self
collection = NameCollection(["Alice", "Bob", "Charlie"])
for name in collection:
print(name)
Alice
Bob
Charlie
Advance singleton 1
We can access named objects, and these objects are guaranteed to be the same so long as you access the object using a specific unique name. This pattern is called a Multiton. Here is how you can define one.
class Multiton:
_instances = {}
@classmethod
def get_instance(cls, key):
if key not in cls._instances:
# Create and store a new instance for the given key
cls._instances[key] = cls(key)
return cls._instances[key]
def __init__(self, key):
if key in self._instances:
raise ValueError("Instance already exists for this key!")
self.key = key
# Usage
instance1 = Multiton.get_instance("config1")
instance2 = Multiton.get_instance("config2")
same_instance = Multiton.get_instance("config1")
print(instance1 is same_instance) # True
print(instance1 is instance2) # False
Advanced singleton 2
Here is another example. You have several kinds of servers to serve different kinds of objects. They all provide somewhat the same interface. So, one way to manage such a list of objects is to use what is called a Registry pattern.
class Registry:
def __init__(self):
self._registry = {}
def register(self, name, obj):
if name in self._registry:
raise ValueError(f"An object is already registered under the name '{name}'")
self._registry[name] = obj
def get(self, name):
return self._registry.get(name, None)
def unregister(self, name):
if name in self._registry:
del self._registry[name]
# Example usage
registry = Registry()
class DocServer:
def __init__(self, args): ...
class ImgServer:
def __init__(self, args): ...
# Registering objects
registry.register("documentserver", DocServer({"host": "localhost", "port": 5432}))
registry.register("imageserver", ImgServer({"host": "imgur", "port": 80}))
# Retrieving objects
doc_server = registry.get("documentserver")
imgserver = registry.get("imageserver")
Notice how the registry pattern is very reminiscent of the multiton pattern? The main difference between registry and multiton is that multiton also handles the creation of registered objects, while registry does not include that responsibility.
Back to super node: Behavioural Patterns
Design_Pattern Behavioural_Design_Patterns SOFT2201 Iterator_Pattern