Iterator Pattern

The Iterator Pattern is a Behavioral Design Pattern that provides a way to sequentially access elements in a collection without exposing the collection’s underlying structure. It allows the client to traverse a collection of elements one by one, regardless of how the collection is implemented (e.g., an array, a list, or a custom data structure). The Iterator Pattern is particularly useful when you want to access a collection’s elements in a consistent way without needing to know the specifics of its internal structure.

The pattern is widely used in programming languages and libraries, where iterators help in handling collections, making traversal logic more manageable and decoupling the iteration process from the data structure.

Key Characteristics of Iterator Pattern

  1. Encapsulation of Traversal Logic:
    • The Iterator Pattern encapsulates the traversal logic, allowing clients to use the iterator without knowing how the elements are stored within the collection.
  2. Uniform Interface for Accessing Collections:
    • The pattern provides a uniform way to access elements in different types of collections, making it easier to work with arrays, lists, and custom collections in a consistent manner.
  3. Supports Multiple Iterations:
    • Multiple iterators can be created for the same collection, allowing for simultaneous iterations with different traversal behaviors (e.g., forward, backward).
  4. Decouples Collection and Traversal:
    • The Iterator Pattern separates the collection from the traversal mechanism, promoting the Single Responsibility Principle. This separation allows changes to the collection’s implementation without affecting the client’s access method.
  5. Easy to Implement Custom Iterations:
    • The pattern makes it easy to implement different types of iterations, such as reverse iteration, filtered iteration, or conditional iteration.
  6. Supports Aggregation and Iteration Together:
    • The pattern is commonly used with collections that aggregate multiple items, enabling iteration without exposing the collection’s internal details.

Iterator Pattern in Node.js

In Node.js, the Iterator Pattern is frequently used in libraries and custom collections to allow iteration over various data structures. Node.js natively supports iterators through the [Symbol.iterator] method, enabling objects to work seamlessly with for...of loops and other iterable functions. Let’s illustrate this pattern with an example of a custom Book Collection that we can iterate over.

Example Scenario: Book Collection Iterator

In this example, we’ll create a BookCollection class that stores a list of books and provides a custom iterator to iterate over the collection.

Step 1: Define the Book Collection

The BookCollection class will hold a list of books and implement the iterator using the [Symbol.iterator] method, allowing us to iterate over it using the for...of loop.

// Book class to represent each book
class Book {
  constructor(title, author) {
    this.title = title;
    this.author = author;
  }
}

// BookCollection class with an iterator
class BookCollection {
  constructor() {
    this.books = [];
  }

  addBook(book) {
    this.books.push(book);
  }

  // Implementing the iterator
  [Symbol.iterator]() {
    let index = 0;
    const books = this.books;

    return {
      next() {
        if (index < books.length) {
          return { value: books[index++], done: false };
        } else {
          return { done: true };
        }
      },
    };
  }
}

Step 2: Using the Iterator

Now we can create a BookCollection, add books to it, and iterate over the collection using the for...of loop.

// Creating a book collection and adding books
const library = new BookCollection();
library.addBook(new Book("To Kill a Mockingbird", "Harper Lee"));
library.addBook(new Book("1984", "George Orwell"));
library.addBook(new Book("The Great Gatsby", "F. Scott Fitzgerald"));

// Using the iterator to traverse the collection
for (const book of library) {
  console.log(`Title: ${book.title}, Author: ${book.author}`);
}

Output:

Title: To Kill a Mockingbird, Author: Harper Lee
Title: 1984, Author: George Orwell
Title: The Great Gatsby, Author: F. Scott Fitzgerald

Explanation:

  • The BookCollection class implements the iterator interface by defining the [Symbol.iterator] method.
  • The for...of loop automatically uses the custom iterator to traverse the collection, allowing us to access each Book in the collection one by one.

Real-World Examples of Iterator Pattern

Node.js Streams:

const fs = require("fs");

async function readFile() {
  const stream = fs.createReadStream("example.txt", { encoding: "utf8" });

  for await (const chunk of stream) {
    console.log(chunk);
  }
}

readFile();

Custom Collection Iteration:

In applications that use custom data structures, the Iterator Pattern is used to implement custom traversal logic. For example, a binary tree or graph data structure might use the Iterator Pattern to allow traversal in different orders (e.g., in-order, pre-order, post-order).

Databases and Result Sets:

Database APIs often use iterators to handle large result sets, allowing applications to process one record at a time without loading everything into memory. For instance, MongoDB’s find method returns a cursor that uses the Iterator Pattern to fetch documents lazily.

File System Traversal:

File systems often expose directories and files as iterators. For example, Node.js’ fs.readdir can be used to iterate over files in a directory, processing each file without loading the entire directory structure into memory.

Pagination in APIs:

In REST APIs, iterators are used to handle paginated data. For instance, a client might receive a paginated response and use an iterator to request additional pages only when needed, making data fetching efficient.

Looping over DOM Elements:

In web development, frameworks and libraries often use the Iterator Pattern to traverse and manipulate collections of DOM elements. This allows developers to perform batch operations on multiple elements efficiently.

Game Development (Object Management):

In games, the Iterator Pattern can be used to manage and iterate over collections of game objects like enemies, players, or items. Each object can be updated, rendered, or processed in each game loop iteration.

Conclusion

The Iterator Pattern provides a structured approach to traverse and access elements within a collection without exposing its internal structure. By defining a standard way to iterate over elements, this pattern makes it easy to handle collections uniformly, regardless of how they are implemented.

In Node.js, the Iterator Pattern is widely used with built-in objects like arrays, maps, sets, and streams, as well as in custom collections. It is an essential pattern for creating iterable data structures, enabling sequential access to collection elements in a flexible, encapsulated way. From handling database records to processing paginated API data or traversing file systems, the Iterator Pattern simplifies data access and enhances the readability and flexibility of code that works with collections.

Leave a comment

I’m Tran Minh

Hi, I’m Trần Minh, a Solution Architect passionate about crafting innovative and efficient solutions that make technology work seamlessly for you. Whether you’re here to explore the latest in tech or just to get inspired, I hope you find something that sparks joy and curiosity. Let’s embark on this exciting journey together!

Let’s connect