-
Notifications
You must be signed in to change notification settings - Fork 1
Iterator Pattern
The iterator pattern is a behavioral design pattern that allows you to traverse a collection of objects without exposing its underlying representation. It's useful when you need to iterate over a complex data structure, such as a list or a tree, without having to modify the underlying structure.
Think of the iterator pattern when you're working with complex data structures and need to iterate over them in a specific way. For example, when you're working with a large dataset and need to process each item in a specific order, or when you're working with a hierarchical data structure and need to traverse it recursively.
The easiest way to understand the intent of the Iterator pattern is to look at examples of where it is useful.
The iterator pattern is commonly used when working with a database, you might use an iterator to iterate over a large dataset without having to load the entire dataset into memory.
Before we jump into a practical example it is necessary to understand why we need an iterator to traverse a complex data structure.
Decoupling / Improved maintainability / Abstraction / Flexibility: The iterator is decoupled from the specific implementation of the data structure, making it easier to change the data structure or switch to a different implementation. We can store our data as an array or a linked list or anything else, it doesn't matter as the iterator provides an abstraction layer between the data structure and the code that uses the data, making it easier to switch between different implementations. As the iterator is responsible for traversing through the data and the rest of system is unaffected. The iterator makes it easier to modify or replace the data structure without affecting the code that uses the data.
first we understand the moving parts in the Iterator pattern:
-
Aggregate: This is the class that contains the data structure that needs to be iterated over. The Aggregate class provides a method to create an Iterator object.
-
Iterator: This is the class that implements the iteration logic. The Iterator class provides methods to iterate over the data structure, such as
hasNext()andnext(). -
Concrete Iterator: This is a subclass of the Iterator class that is specific to the Aggregate class. The Concrete Iterator class provides the implementation of the iteration logic for the specific Aggregate class.
-
Client: This is the class that uses the Iterator pattern to iterate over the data structure. The Client class uses the Iterator object to iterate over the data structure.
AGENDA : Use the iterator pattern to hide the underlying representation of the collection/data, from the means of accessing the collection/data. Iterator pattern uses stand-alone objects called iterators which has methods for iterating over the data structure.
- Provides flexibility to dynamically change the iterator or add new iterators without affecting the client code.
- Supports open/closed principle by allowing new iterators to be added without modifying existing code.
Step 1: Define the Aggregate class:
- Create a class that contains the data structure that needs to be iterated over.
- Define a method to create an Iterator object.
class Aggregate {
private collection: T[];
constructor(collection: T[]) {
this.collection = collection;
}
public iterator(): Iterator {
return new Iterator(this.collection);
}
}Step 2: Define the Iterator interface:
- Create an interface that defines the methods for iterating over the data structure.
- The methods should include
hasNext()andnext().
interface Iterator<T> {
next(): T | undefined;
hasNext(): boolean;
remove(): void;
}Step 3: Define the Concrete Iterator class:
- Create a subclass of the Iterator class that is specific to the Aggregate class.
- Implement the iteration logic for the specific Aggregate class.
class MyIterator<T> implements Iterator<T> {
private collection: T[];
private index: number;
constructor(collection: T[]) {
this.collection = collection;
this.index = 0;
}
next(): T | undefined {
if (!this.hasNext()) {
return undefined;
}
const item = this.collection[this.index];
this.index++;
return item;
}
hasNext(): boolean {
return this.index < this.collection.length;
}
remove(): void {
if (!this.hasNext()) {
throw new Error("Cannot remove from an empty iterator");
}
this.collection.splice(this.index - 1, 1);
}
}Step 4: Implement the Aggregate class:
- Implement the method to create an Iterator object.
- Use the Concrete Iterator class to iterate over the data structure.
class MyAggregate {
private collection: T[];
constructor(collection: T[]) {
this.collection = collection;
}
public iterator(): MyIterator {
return new MyIterator(this.collection);
}
}Step 5: Implement the Client class:
- Use the Iterator object to iterate over the data structure.
- Use the
hasNext()andnext()methods to iterate over the data structur
class MusicPlayer {
public playMusic(playlist: Playlist) {
const iterator = playlist.iterator();
while (iterator.hasNext()) {
const song = iterator.next();
console.log(`Playing song: ${song.name}`);
}
}
}Example usage
const songs: Song[] = [
{ name: 'Song 1' },
{ name: 'Song 2' },
{ name: 'Song 3' },
];
const playlist = new Playlist(songs);
const musicPlayer = new MusicPlayer();
musicPlayer.playMusic(playlist);In this example, the Playlist class is the Aggregate class, the Iterator interface is the Iterator interface, the PlaylistIterator class is the Concrete Iterator class, and the MusicPlayer class is the Client class.
The iterator pattern works by providing a way to iterate over a collection of objects without exposing the underlying representation of the collection. The iterator class implements the next() method, which returns the next item in the collection, and the hasNext() method, which checks if there are more items in the collection. The remove() method is used to remove the current item from the collection.
Use the iterator pattern when you need to iterate over a complex data structure and don't want to expose the underlying representation of the data structure. The iterator pattern is particularly useful when working with large datasets or hierarchical data structures.