Skip to main content

Introduction To The Iterator Pattern

The Iterator pattern is a design pattern that allows sequential access to elements in a collection, without exposing its underlying representation. It provides a way to access the elements of an aggregate object sequentially without exposing the underlying details.

In many cases, iterators can be used to simplify the code that iterates over a collection. They provide a common interface that allows you to swap out different collections easily, which can make your code more modular and easier to maintain.

Important Components Of Iterator Pattern

An Iterator usually implements two main methods:

  • next(): Returns the next element in the sequence. After every call, the internal pointer increases.
  • hasNext(): Checks if there are more elements in the sequence to iterate over.

Classic Implementation Of The Iterator Pattern

Here's a basic implementation of an iterator in TypeScript for an array of numbers:

class ArrayIterator {
private collection: number[];
private position: number = 0;

constructor(collection: number[]) {
this.collection = collection;
}

public next(): number {
// Get the next element from the collection
const result: number = this.collection[this.position];
this.position += 1;
return result;
}

public hasNext(): boolean {
// Check if there are more elements in the collection
return this.position < this.collection.length;
}
}

// Using the ArrayIterator
const array = [1, 2, 3, 4, 5];
const iterator = new ArrayIterator(array);
while (iterator.hasNext()) {
console.log(iterator.next()); // Logs 1, 2, 3, 4, 5
}

In this example, the ArrayIterator allows us to iterate over an array of numbers. This could easily be generalized to any type of collection (e.g., linked list, tree, graph, etc.) by modifying the next() and hasNext() methods to handle the specific structure of the collection.

This example could also be extended to handle collections of any type, not just numbers. This can be achieved using TypeScript's generics feature:

class ArrayIterator<T> {
private collection: T[];
private position: number = 0;

constructor(collection: T[]) {
this.collection = collection;
}

public next(): T {
const result: T = this.collection[this.position];
this.position += 1;
return result;
}

public hasNext(): boolean {
return this.position < this.collection.length;
}
}

// Using the ArrayIterator with a string array
const stringArray = ["Hello", "World", "!"];
const stringIterator = new ArrayIterator<string>(stringArray);
while (stringIterator.hasNext()) {
console.log(stringIterator.next()); // Logs 'Hello', 'World', '!'
}

In this example, the ArrayIterator<T> can iterate over an array of any type T, not just numbers. This is an example of how the Iterator pattern can be used to create flexible, reusable code.

When To Use The Iterator Pattern

The Iterator pattern is used to provide a way to access the elements of an aggregate object (like an array or a tree) sequentially without exposing its underlying representation.

You might consider using the Iterator pattern when you encounter the following situations, patterns, or code smells:

Complex Navigation Logic

"Complex Navigation Logic" refers to situations where the logic for traversing or navigating a data structure becomes complicated and intertwined with the business logic of your application. This can happen when you're working with complex data structures like trees or graphs, or even with more complex composite structures.

When the navigation logic becomes complicated, it becomes harder to read and understand the code, harder to make changes without introducing bugs, and harder to reuse code across different parts of the application. These are all signs that it might be beneficial to refactor your code to encapsulate the navigation logic into its own class or method, which is exactly what the Iterator pattern does.

A common example of this is a tree structure, like a file system. Here's an example:

  • Consider a file system where each directory can contain files or other directories. You might have business requirements like:
    • Calculate the total size of all files in a directory.
    • Find all files in a directory that were modified in the last week.
    • Display a list of all files in a directory.

In each of these cases, you would need to traverse the directory tree, which can be done using either a depth-first or breadth-first algorithm. But the logic for traversing the tree is the same in each case, and is independent of the business logic for calculating sizes, filtering by modification date, or displaying file names.

Without an Iterator, you might find yourself repeating the traversal logic in each part of your application that needs to iterate over the file system. This leads to code duplication and tight coupling between your business logic and the specific traversal algorithm you're using.

By using the Iterator pattern, you can encapsulate the traversal logic into an Iterator object, and then use this object in each part of your application that needs to iterate over the file system. This makes your code cleaner, easier to understand, and more flexible. For example, if you later decide to switch from a depth-first to a breadth-first traversal, you only need to change the Iterator, not every part of your application that iterates over the file system.

Multiple Traversal Algorithms

"Multiple Traversal Algorithms" refers to a situation where you have different ways or algorithms to traverse a data structure, and you need to be able to switch between them.

For example, consider a binary tree data structure. There are several ways to traverse a binary tree:

  1. In-order Traversal: In this traversal method, the left subtree is visited first, then the root and later the right subtree.

  2. Pre-order Traversal: In this traversal method, the root is visited first, then the left subtree and finally the right subtree.

  3. Post-order Traversal: In this traversal method, the root is the last to be visited. The left subtree is visited first, then the right subtree and finally the root node.

  4. Level-order Traversal (or Breadth-First): In this traversal method, all nodes of a particular depth are visited before visiting nodes at the next depth level.

These different traversals are useful in different situations - for example, in-order traversal of a binary search tree will visit the nodes in ascending order, while pre-order traversal can be used to make a copy of the tree.

Without an iterator, each different traversal algorithm would likely be implemented as a separate method on the tree class. If you wanted to change traversals, you'd have to change the method you call. If you wanted to use the same traversal in different parts of your code, you'd have to duplicate code.

The Iterator pattern can help here by encapsulating each traversal algorithm into a separate Iterator class. To change traversals, you just create an instance of a different Iterator class. You can pass Iterator objects around your code, allowing you to use the same traversal in different places without duplicating code.

In a real world scenario, suppose you have a social media platform and each user is a node in a graph data structure. You might have to analyze the connections ( edges) in a variety of ways, for example, breadth-first to suggest friends within a certain degree of separation (people your friends are connected with), or depth-first to find out 'six degrees of separation' between two users. These different kinds of traversal methods can be easily achieved by different types of iterators without changing the underlying user graph.

Accessing Elements of a Collection without Exposing its Structure

"Accessing Elements of a Collection without Exposing its Structure" refers to situations where we want clients to be able to access or iterate over the elements of a collection, without needing to know about the underlying data structure or how it is implemented.

This is important because it allows us to change the implementation of our data structure without affecting the clients that use it. This is a principle known as encapsulation – one of the key principles of object-oriented programming.

Consider a simple example: Let's say we have a collection of books and we want to allow clients to iterate over them (for example, to print out their titles). If we expose the underlying array that we're using to store the books, clients would need to know that it's an array and how to iterate over an array.

This leads to a couple of problems:

  1. Tight Coupling: Our client code becomes tightly coupled to the implementation of our collection. If we ever want to change our implementation (for example, to use a linked list instead of an array), we would also need to update all of the client code that uses it.

  2. Violation of Encapsulation: By exposing the internal workings of our collection, we're also exposing more than we should and potentially allowing clients to modify our collection in ways we didn't intend.

The Iterator pattern solves these problems by providing a way to access the elements of a collection sequentially without exposing the underlying data structure. In the case of our book collection, we could provide an iterator that allows clients to iterate over the books without knowing whether they're stored in an array, a linked list, or some other data structure.

The clients only need to know how to work with the iterator, and they're isolated from any changes to the underlying collection structure. This leads to more flexible and maintainable code.

Different Collections with Same Traversal

"Different Collections with Same Traversal" refers to a situation where you have multiple collection types that need to be traversed in the same manner.

For instance, consider an application that works with several collections like an array, a linked list, and a binary tree. Even though these collections are different in their structures and implementation, they might share a common operation - iteration over their elements. However, the iteration process for each of these collections would be different due to their unique internal structure.

Without an iterator, clients that want to traverse these collections would have to know about their internal structures and implement separate traversal logic for each collection type. This leads to code duplication and violation of encapsulation as the internal structure of the collections is exposed to the client.

The Iterator pattern can help here by providing a common interface for traversing different collection types. We can create a specific iterator for each collection type (Array Iterator, LinkedList Iterator, BinaryTree Iterator, etc.) that implements this interface.

Clients can then use this interface to traverse any collection, regardless of its actual type. They do not need to know about the internal structure of the collections, and they can reuse the same code for traversing different types of collections. This leads to more maintainable and flexible code.

A real-world example could be a social network service where user data might be stored in different types of data structures - an array for active users, a tree for user hierarchies (in case of corporate networks), and a linked list for users in a particular group. If the application needs to perform operations like broadcasting a message to all users irrespective of their grouping, an iterator pattern can provide a uniform way to traverse through all these collections.

Remember, the Iterator pattern isn't always the best solution. It can add unnecessary complexity if you're only working with simple collections or if you never need to switch between different traversal algorithms. Like all design patterns, you should use it judiciously and where it's most appropriate.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

Time To Transition From JavaScript To TypeScript

Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.

SEE COURSE DETAILS

What Can You Do Next 🙏😊

If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.

YouTube @cloudaffle