Open In App

Iterator Design Pattern

Last Updated : 02 Apr, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

The Iterator pattern is a widely used design pattern in software development that provides a way to access the elements of an aggregate object (such as a list or collection) sequentially without exposing its underlying representation.

Iterator-design-pattern

What is the Iterator Design Pattern?

The Iterator design pattern is a behavioral design pattern that provides a way to access the elements of an aggregate object (like a list) sequentially without exposing its underlying representation. It defines a separate object, called an iterator, which encapsulates the details of traversing the elements of the aggregate, allowing the aggregate to change its internal structure without affecting the way its elements are accessed.

  • Iterator Pattern is a relatively simple and frequently used design pattern. There are a lot of data structures/collections available in every language.
  • Each collection must provide an iterator that lets it iterate through its objects. However, while doing so it should make sure that it does not expose its implementation.

Suppose we are building an application that requires us to maintain a list of notifications. Eventually, some parts of your code will require you to iterate over all notifications. If we implemented your collection of notifications as an array you would iterate over them as:

Java
// If a simple array is used to store notifications
for (int i = 0; i < notificationList.length; i++)
     Notification notification = notificationList[i]);


And if it were some other collection like set, tree, etc. way of iterating would change slightly. Now, what if we build an iterator that provides a generic way of iterating over a collection independent of its type.

Java
// Create an iterator
Iterator iterator = notificationList.createIterator();
// It wouldn’t matter if list is Array or ArrayList or
// anything else.
while (iterator.hasNext())
{
    Notification notification = iterator.next());
}

Components of Iterator Design Pattern

The Iterator design pattern consists of several components:

Iterator2


1. Iterator Interface/Abstract Class

Defines the interface for accessing and traversing elements in the collection. It typically includes methods like hasNext(), next(), and optionally remove().

2. Concrete Iterator

Implements the Iterator interface and maintains the current position in the traversal of the aggregate. It provides the actual implementation for the traversal operations defined in the Iterator interface.

3. Aggregate Interface/Abstract Class

Defines the interface for creating an Iterator object. It typically includes a method like createIterator() that returns an Iterator object for the collection.

4. Concrete Aggregate

Implements the Aggregate interface and represents the collection of objects. It provides the implementation for creating an Iterator object that can traverse its elements.

Iterator Design Pattern example

Problem Statement:

Let’s say we have a collection of employees in a company, and we want to iterate over the employees to calculate their total salary. However, the employees are stored in different types of collections (arrays, lists, etc.), and we want to iterate over them without exposing the underlying collection types.

Iterator1

Benefit of using the Iterator Pattern

The Iterator pattern allows us to access the elements of a collection sequentially without exposing its underlying representation. It provides a way to iterate over a collection regardless of its internal structure.

Below is the code of above problem statement using Iterator Pattern:

Let’s break down into the component wise code:

1. Iterator Interface

Defines the methods for accessing and traversing the collection.

Java
interface Iterator<T> {
    boolean hasNext();
    T next();
}

2. Aggregate Interface

Defines the method for creating an iterator.

Java
interface Aggregate<T> {
    Iterator<T> createIterator();
}

3. Concrete Iterator

Implements the Iterator interface and provides the actual iteration logic.

Java
class EmployeeIterator implements Iterator<Employee> {
    private int currentIndex = 0;
    private List<Employee> employees;

    public EmployeeIterator(List<Employee> employees) {
        this.employees = employees;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < employees.size();
    }

    @Override
    public Employee next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        return employees.get(currentIndex++);
    }
}

4. Concrete Aggregate

Implements the Aggregate interface and provides the method to create an iterator for the collection.

Java
class Company implements Aggregate<Employee> {
    private List<Employee> employees;

    public Company(List<Employee> employees) {
        this.employees = employees;
    }

    @Override
    public Iterator<Employee> createIterator() {
        return new EmployeeIterator(employees);
    }
}

Complete code for the above example

This code demonstrates how the Iterator pattern can be used to iterate over a collection of employees in a company, regardless of the internal storage of the employees. Below is the complete code for the above example:

Java
import java.util.*;

// Employee class
class Employee {
    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }
}

// Iterator interface
interface Iterator<T> {
    boolean hasNext();
    T next();
}

// Aggregate interface
interface Aggregate<T> {
    Iterator<T> createIterator();
}

// Concrete Iterator
class EmployeeIterator implements Iterator<Employee> {
    private int currentIndex = 0;
    private List<Employee> employees;

    public EmployeeIterator(List<Employee> employees) {
        this.employees = employees;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < employees.size();
    }

    @Override
    public Employee next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        return employees.get(currentIndex++);
    }
}

// Concrete Aggregate
class Company implements Aggregate<Employee> {
    private List<Employee> employees;

    public Company(List<Employee> employees) {
        this.employees = employees;
    }

    @Override
    public Iterator<Employee> createIterator() {
        return new EmployeeIterator(employees);
    }
}

// Main class
public class Main {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("Alice", 50000));
        employees.add(new Employee("Bob", 60000));
        employees.add(new Employee("Charlie", 70000));

        Company company = new Company(employees);
        Iterator<Employee> iterator = company.createIterator();

        double totalSalary = 0;
        while (iterator.hasNext()) {
            totalSalary += iterator.next().getSalary();
        }

        System.out.println("Total salary: " + totalSalary);
    }
}
JavaScript
class Employee {
    constructor(name, salary) {
        this.name = name;
        this.salary = salary;
    }
}
// Iterator interface
class Iterator {
    constructor(collection) {
        this.collection = collection;
        this.currentIndex = 0;
    }
    hasNext() {
        return this.currentIndex < this.collection.length;
    }
    next() {
        if (!this.hasNext()) {
            throw new Error('No such element');
        }
        return this.collection[this.currentIndex++];
    }
}
// Aggregate interface
class Aggregate {
    createIterator() {
        return null;
    }
}
// Concrete Iterator
class EmployeeIterator extends Iterator {
    constructor(collection) {
        super(collection);
    }
}
// Concrete Aggregate
class Company extends Aggregate {
    constructor(collection) {
        super();
        this.collection = collection;
    }
    createIterator() {
        return new EmployeeIterator(this.collection);
    }
}
// Main function
function main() {
    const employees = [
        new Employee("Alice", 50000),
        new Employee("Bob", 60000),
        new Employee("Charlie", 70000)
    ];
    const company = new Company(employees);
    const iterator = company.createIterator();
    let totalSalary = 0;
    while (iterator.hasNext()) {
        totalSalary += iterator.next().salary;
    }
    console.log("Total salary: " + totalSalary);
}
main();
Output
Total salary: 180000.0

When to use Iterator Design Pattern

  • Need for sequential access: Use the Iterator pattern when you need to access elements of a collection sequentially without exposing its underlying representation. This pattern provides a uniform way to iterate over different types of collections.
  • Decoupling iteration logic: Use the Iterator pattern when you want to decouple the iteration logic from the collection. This allows the collection to change its internal structure without affecting the way its elements are accessed.
  • Support for multiple iterators: Use the Iterator pattern when you need to support multiple iterators over the same collection. Each iterator maintains its own iteration state, allowing multiple iterations to occur concurrently.
  • Simplifying client code: Use the Iterator pattern to simplify client code that iterates over a collection. Clients only need to interact with the iterator interface, abstracting away the complexity of the collection’s internal structure.

When to not use Iterator Design Pattern

  • When the collection is not accessed sequentially: If the collection is not accessed sequentially, using the Iterator pattern may add unnecessary complexity. Consider other patterns or direct access methods based on the specific access patterns required.
  • When the collection structure is fixed: If the structure of the collection is fixed and unlikely to change, using the Iterator pattern may be overkill. Direct access methods may be more appropriate and simpler to implement.
  • When performance is critical: In performance-critical applications, the overhead of using iterators may be significant, especially if the collection is large. In such cases, consider direct access methods for better performance.
  • When the language provides better alternatives: Some languages provide built-in constructs or libraries that offer more efficient ways to iterate over collections. In such cases, using these alternatives may be more appropriate than implementing the Iterator pattern from scratch.




Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads