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