Skip to main content

Iterator Pattern 🔄

Definition: The Iterator pattern provides a way to access elements of a collection sequentially without exposing its underlying representation.

🎯 Intent

Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation. The iterator encapsulates the traversal logic.

🤔 Problem

You need to traverse a collection of objects without:

  • Exposing Internal Structure: Client shouldn't know how collection stores elements
  • Multiple Traversals: Need to support different ways of traversing the same collection
  • Complex Navigation: Collection might have complex internal structure (trees, graphs)
  • Uniform Interface: Different collections should provide same traversal interface

Direct access to collection internals creates tight coupling and limits flexibility.

💡 Solution

The Iterator pattern suggests extracting traversal behavior into separate iterator objects. Each iterator implements a standard interface for traversing collections, providing methods like next(), hasNext(), etc.

🏗️ Structure

Iterator (interface)
├── +next(): Element
├── +hasNext(): boolean
└── +current(): Element

ConcreteIterator implements Iterator
├── -collection: Collection
├── -position: number
├── +next(): Element
├── +hasNext(): boolean
└── +current(): Element

Collection (interface)
└── +createIterator(): Iterator

ConcreteCollection implements Collection
├── -items: Array
├── +createIterator(): ConcreteIterator
└── +getItems(): Array

💻 Simple Example

Book Collection Iterator

// Iterator Interface
class Iterator {
next() {
throw new Error("next() method must be implemented");
}

hasNext() {
throw new Error("hasNext() method must be implemented");
}

current() {
throw new Error("current() method must be implemented");
}

reset() {
throw new Error("reset() method must be implemented");
}
}

// Book class
class Book {
constructor(title, author, year, genre) {
this.title = title;
this.author = author;
this.year = year;
this.genre = genre;
}

toString() {
return `"${this.title}" by ${this.author} (${this.year}) - ${this.genre}`;
}
}

// Concrete Iterator - Forward Iterator
class BookIterator extends Iterator {
constructor(books) {
super();
this.books = books;
this.position = 0;
}

next() {
if (this.hasNext()) {
const book = this.books[this.position];
this.position++;
return book;
}
return null;
}

hasNext() {
return this.position < this.books.length;
}

current() {
return this.position > 0 ? this.books[this.position - 1] : null;
}

reset() {
this.position = 0;
console.log("📖 Iterator reset to beginning");
}
}

// Concrete Iterator - Reverse Iterator
class ReverseBookIterator extends Iterator {
constructor(books) {
super();
this.books = books;
this.position = books.length - 1;
}

next() {
if (this.hasNext()) {
const book = this.books[this.position];
this.position--;
return book;
}
return null;
}

hasNext() {
return this.position >= 0;
}

current() {
return this.position < this.books.length - 1 ? this.books[this.position + 1] : null;
}

reset() {
this.position = this.books.length - 1;
console.log("📖 Reverse iterator reset to end");
}
}

// Collection Interface
class Collection {
createIterator() {
throw new Error("createIterator() method must be implemented");
}
}

// Concrete Collection - Book Library
class BookLibrary extends Collection {
constructor() {
super();
this.books = [];
}

addBook(book) {
this.books.push(book);
console.log(`📚 Added book: ${book.title}`);
}

removeBook(title) {
const index = this.books.findIndex(book => book.title === title);
if (index !== -1) {
const removedBook = this.books.splice(index, 1)[0];
console.log(`🗑️ Removed book: ${removedBook.title}`);
return true;
}
console.log(`❌ Book not found: ${title}`);
return false;
}

createIterator() {
console.log("🔄 Creating forward iterator");
return new BookIterator(this.books);
}

createReverseIterator() {
console.log("🔄 Creating reverse iterator");
return new ReverseBookIterator(this.books);
}

getBookCount() {
return this.books.length;
}

displayInfo() {
console.log(`📚 Library contains ${this.books.length} books`);
}
}

// Usage
console.log("=== Book Library Iterator Demo ===\n");

console.log("Setting up book library:");
console.log("-".repeat(25));

const library = new BookLibrary();

// Add books
const books = [
new Book("The Great Gatsby", "F. Scott Fitzgerald", 1925, "Classic Fiction"),
new Book("To Kill a Mockingbird", "Harper Lee", 1960, "Classic Fiction"),
new Book("1984", "George Orwell", 1949, "Dystopian Fiction"),
new Book("Pride and Prejudice", "Jane Austen", 1813, "Romance"),
new Book("The Catcher in the Rye", "J.D. Salinger", 1951, "Coming-of-age")
];

books.forEach(book => library.addBook(book));

library.displayInfo();

console.log("\n" + "=".repeat(50) + "\n");

console.log("Iterating through books (forward):");
console.log("-".repeat(35));

const iterator = library.createIterator();
let bookNumber = 1;

while (iterator.hasNext()) {
const book = iterator.next();
console.log(`${bookNumber}. ${book.toString()}`);
bookNumber++;
}

console.log("\n" + "-".repeat(40) + "\n");

console.log("Iterating through books (reverse):");
console.log("-".repeat(35));

const reverseIterator = library.createReverseIterator();
bookNumber = 1;

while (reverseIterator.hasNext()) {
const book = reverseIterator.next();
console.log(`${bookNumber}. ${book.toString()}`);
bookNumber++;
}

console.log("\n" + "-".repeat(40) + "\n");

console.log("Demonstrating iterator reset:");
console.log("-".repeat(30));

iterator.reset();
console.log("First two books after reset:");

for (let i = 0; i < 2 && iterator.hasNext(); i++) {
const book = iterator.next();
console.log(` ${i + 1}. ${book.title}`);
}

🌟 Real-World Example

Playlist Iterator with Different Modes

// Song class
class Song {
constructor(title, artist, duration, genre) {
this.title = title;
this.artist = artist;
this.duration = duration; // in seconds
this.genre = genre;
this.playCount = 0;
}

play() {
this.playCount++;
console.log(`🎵 Playing: "${this.title}" by ${this.artist} (${this.formatDuration()}) [Play #${this.playCount}]`);
}

formatDuration() {
const minutes = Math.floor(this.duration / 60);
const seconds = this.duration % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}

toString() {
return `"${this.title}" by ${this.artist} (${this.formatDuration()}) - ${this.genre}`;
}
}

// Abstract Iterator for playlist
class PlaylistIterator {
constructor(songs) {
this.songs = songs;
this.position = 0;
}

next() {
throw new Error("next() method must be implemented");
}

hasNext() {
throw new Error("hasNext() method must be implemented");
}

current() {
return this.position > 0 ? this.songs[this.position - 1] : null;
}

reset() {
throw new Error("reset() method must be implemented");
}

getPosition() {
return this.position;
}
}

// Sequential Iterator
class SequentialIterator extends PlaylistIterator {
next() {
if (this.hasNext()) {
const song = this.songs[this.position];
this.position++;
return song;
}
return null;
}

hasNext() {
return this.position < this.songs.length;
}

reset() {
this.position = 0;
console.log("🔄 Sequential iterator reset");
}
}

// Shuffle Iterator
class ShuffleIterator extends PlaylistIterator {
constructor(songs) {
super(songs);
this.shuffledIndices = this.createShuffledIndices();
this.position = 0;
}

createShuffledIndices() {
const indices = Array.from({length: this.songs.length}, (_, i) => i);

// Fisher-Yates shuffle algorithm
for (let i = indices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[indices[i], indices[j]] = [indices[j], indices[i]];
}

console.log("🔀 Playlist shuffled");
return indices;
}

next() {
if (this.hasNext()) {
const songIndex = this.shuffledIndices[this.position];
const song = this.songs[songIndex];
this.position++;
return song;
}
return null;
}

hasNext() {
return this.position < this.shuffledIndices.length;
}

reset() {
this.position = 0;
this.shuffledIndices = this.createShuffledIndices();
console.log("🔄 Shuffle iterator reset with new shuffle");
}
}

// Repeat Iterator (repeats the playlist indefinitely)
class RepeatIterator extends PlaylistIterator {
constructor(songs, maxRepeats = 3) {
super(songs);
this.maxRepeats = maxRepeats;
this.currentRepeat = 0;
}

next() {
if (this.hasNext()) {
const song = this.songs[this.position % this.songs.length];
this.position++;

// Check if we completed a full cycle
if (this.position % this.songs.length === 0) {
this.currentRepeat++;
console.log(`🔁 Completed repeat ${this.currentRepeat}/${this.maxRepeats}`);
}

return song;
}
return null;
}

hasNext() {
return this.currentRepeat < this.maxRepeats;
}

reset() {
this.position = 0;
this.currentRepeat = 0;
console.log("🔄 Repeat iterator reset");
}

getCurrentRepeat() {
return this.currentRepeat;
}
}

// Genre Filter Iterator
class GenreFilterIterator extends PlaylistIterator {
constructor(songs, genre) {
super(songs);
this.genre = genre;
this.filteredSongs = songs.filter(song =>
song.genre.toLowerCase() === genre.toLowerCase()
);
this.position = 0;

console.log(`🎯 Genre filter applied: ${genre} (${this.filteredSongs.length} songs)`);
}

next() {
if (this.hasNext()) {
const song = this.filteredSongs[this.position];
this.position++;
return song;
}
return null;
}

hasNext() {
return this.position < this.filteredSongs.length;
}

reset() {
this.position = 0;
console.log(`🔄 Genre filter iterator reset (${this.genre})`);
}
}

// Playlist Collection
class Playlist {
constructor(name) {
this.name = name;
this.songs = [];
this.created = new Date();
}

addSong(song) {
this.songs.push(song);
console.log(`➕ Added to "${this.name}": ${song.title}`);
}

removeSong(title) {
const index = this.songs.findIndex(song => song.title === title);
if (index !== -1) {
const removedSong = this.songs.splice(index, 1)[0];
console.log(`➖ Removed from "${this.name}": ${removedSong.title}`);
return true;
}
return false;
}

// Factory methods for different iterator types
createSequentialIterator() {
return new SequentialIterator(this.songs);
}

createShuffleIterator() {
return new ShuffleIterator(this.songs);
}

createRepeatIterator(maxRepeats = 3) {
return new RepeatIterator(this.songs, maxRepeats);
}

createGenreIterator(genre) {
return new GenreFilterIterator(this.songs, genre);
}

getStats() {
const totalDuration = this.songs.reduce((sum, song) => sum + song.duration, 0);
const genres = [...new Set(this.songs.map(song => song.genre))];

console.log(`📊 Playlist "${this.name}" Stats:`);
console.log(` Songs: ${this.songs.length}`);
console.log(` Total Duration: ${Math.floor(totalDuration / 60)} minutes`);
console.log(` Genres: ${genres.join(', ')}`);
console.log(` Created: ${this.created.toLocaleDateString()}`);
}
}

// Usage
console.log("\n=== Music Playlist Iterator Demo ===\n");

console.log("Creating playlist:");
console.log("-".repeat(18));

const myPlaylist = new Playlist("My Favorites");

// Add songs
const songs = [
new Song("Bohemian Rhapsody", "Queen", 355, "Rock"),
new Song("Stairway to Heaven", "Led Zeppelin", 482, "Rock"),
new Song("Hotel California", "Eagles", 391, "Rock"),
new Song("Billie Jean", "Michael Jackson", 294, "Pop"),
new Song("Like a Rolling Stone", "Bob Dylan", 369, "Folk Rock"),
new Song("Imagine", "John Lennon", 183, "Pop"),
new Song("Sweet Child O' Mine", "Guns N' Roses", 356, "Rock"),
new Song("Thriller", "Michael Jackson", 357, "Pop")
];

songs.forEach(song => myPlaylist.addSong(song));

myPlaylist.getStats();

console.log("\n" + "=".repeat(50) + "\n");

console.log("1. Sequential playback:");
console.log("-".repeat(23));

const sequential = myPlaylist.createSequentialIterator();
let count = 0;
while (sequential.hasNext() && count < 3) {
const song = sequential.next();
song.play();
count++;
}

console.log("\n" + "-".repeat(40) + "\n");

console.log("2. Shuffle mode:");
console.log("-".repeat(15));

const shuffle = myPlaylist.createShuffleIterator();
count = 0;
while (shuffle.hasNext() && count < 3) {
const song = shuffle.next();
song.play();
count++;
}

console.log("\n" + "-".repeat(40) + "\n");

console.log("3. Genre filter (Rock only):");
console.log("-".repeat(28));

const rockIterator = myPlaylist.createGenreIterator("Rock");
while (rockIterator.hasNext()) {
const song = rockIterator.next();
song.play();
}

console.log("\n" + "-".repeat(40) + "\n");

console.log("4. Repeat mode (2 repeats):");
console.log("-".repeat(27));

const repeat = myPlaylist.createRepeatIterator(2);
count = 0;
// Show first few songs and then fast forward to show repeat behavior
while (repeat.hasNext() && count < 5) {
const song = repeat.next();
if (count < 3 || repeat.getCurrentRepeat() > 0) {
song.play();
} else {
console.log(" ... (skipping to show repeat behavior)");
}
count++;
}

// Fast forward to show repeat
while (repeat.hasNext()) {
const song = repeat.next();
if (repeat.getCurrentRepeat() > 0) {
song.play();
break;
}
}

🔧 Simple Tree Iterator Example

// Tree Node
class TreeNode {
constructor(value) {
this.value = value;
this.children = [];
}

addChild(node) {
this.children.push(node);
}

toString() {
return this.value.toString();
}
}

// Tree Iterator (Depth-First)
class TreeIterator {
constructor(root) {
this.stack = root ? [root] : [];
}

next() {
if (!this.hasNext()) {
return null;
}

const node = this.stack.pop();

// Add children to stack in reverse order for correct traversal order
for (let i = node.children.length - 1; i >= 0; i--) {
this.stack.push(node.children[i]);
}

return node;
}

hasNext() {
return this.stack.length > 0;
}
}

// Tree Collection
class Tree {
constructor(rootValue) {
this.root = new TreeNode(rootValue);
}

getRoot() {
return this.root;
}

createIterator() {
return new TreeIterator(this.root);
}
}

// Usage
console.log("\n=== Tree Iterator Demo ===\n");

const tree = new Tree("Root");
const child1 = new TreeNode("Child 1");
const child2 = new TreeNode("Child 2");
const child3 = new TreeNode("Child 3");

tree.root.addChild(child1);
tree.root.addChild(child2);
tree.root.addChild(child3);

child1.addChild(new TreeNode("Child 1.1"));
child1.addChild(new TreeNode("Child 1.2"));
child2.addChild(new TreeNode("Child 2.1"));

console.log("Tree traversal (Depth-First):");
console.log("-".repeat(30));

const treeIterator = tree.createIterator();
let nodeNumber = 1;

while (treeIterator.hasNext()) {
const node = treeIterator.next();
console.log(`${nodeNumber}. ${node.value}`);
nodeNumber++;
}

✅ Pros

  • Uniform Interface: Same interface for different collections
  • Encapsulation: Hides collection's internal structure
  • Multiple Iterators: Can have multiple iterators on same collection
  • Different Traversal Methods: Support various traversal algorithms
  • Single Responsibility: Separates traversal logic from collection logic

❌ Cons

  • Overhead: Additional classes and complexity
  • Performance: May be slower than direct access
  • Memory Usage: Iterator objects consume memory
  • Concurrent Modification: Issues if collection is modified during iteration

🎯 When to Use

  • Collection Traversal: Need to traverse collections without exposing structure
  • Multiple Traversal Methods: Different ways to iterate same collection
  • Uniform Interface: Want same interface for different collection types
  • Complex Collections: Collections with complex internal structure (trees, graphs)
  • Lazy Evaluation: Want to generate elements on demand

🔄 Iterator Types

1. External Iterator (shown in examples)

  • Client controls iteration
  • Calls next(), hasNext() explicitly

2. Internal Iterator

  • Collection controls iteration
  • Client provides callback function
class InternalIteratorCollection {
constructor(items) {
this.items = items;
}

forEach(callback) {
for (let i = 0; i < this.items.length; i++) {
callback(this.items[i], i);
}
}
}

3. Bidirectional Iterator

class BidirectionalIterator {
// ... other methods ...

previous() {
if (this.hasPrevious()) {
this.position--;
return this.collection[this.position];
}
return null;
}

hasPrevious() {
return this.position > 0;
}
}
  • Composite: Often used together for tree traversal
  • Factory Method: Can be used to create different iterator types
  • Command: Iterator operations can be implemented as commands
  • Visitor: Can be combined for complex operations on collections

📚 Further Reading