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;
}
}
🔗 Related Patterns
- 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