Iterator Pattern

Iterator Pattern


WHAT IS ITERATOR PATTERN?

Iterator pattern allows us to traverse elements of a collection without exposing its underlying implementation or representation. The logic of traversal is encapsulated within a separate iterator object. This comes in handy when we have complex data structures like trees or graphs, where iterating can be difficult.

It essentially addresses the issue of code duplication and low maintainability by encapsulating the traversal logic.

So the doctor used his staff as an iterator to see the patients. Everytime one appointment got over, he asked his staff to send the next patient. If there were no patients left, the staff would send no-one and everyone would go home. (There is actually another way to handle this. Can you think about it and leave a comment? Hint: JavaScript does it in this way.)

In other words, the logic of reviewing and sending the patients was encapsulated by the staff.

But How did it help the doctor? (with parrallels to software)

  1. Separation of Concerns: The doctor could now focus on his responsibility of seeing the patients (and possibly curing them for good), leaving the staff responsible for sending (iterating over) the patients. Similarly the collection class can focus on its primary responsibiliy of storing and managing elements while the iterator can take the responsibility of dealing with the iteration. It abides the Single Responsibility Principle.
  2. Avoid Duplication: If the doctor was smart (in doing business) and the staff did a good job (which they did not, I’ll come back to that), he could allow his staff to work for other doctors in exchange for a fee. In this way, other doctors would be able to see more patients efficiently and wouldn’t have to train their staff in a similar manner. In a similar fashion, the traversal logic of any kind of collections would be stored in one particular place (the collections’ iterator) and can be used anywhere easily. And changes in the collection logic can be accomodated here.
  3. Blackboxing: Once the 30 people are set, the doctor doesn’t have to know in what order they arrive. As long as they are in the list, the staff would send them whenever they are available. Similarly, the client doen’t have to know the internal implementation of the collection to work with it. As long as the iterator contract is followed, the client and the collection can easily work with each other.
  4. Different Rules: The staff decides the rules. Like in this case, they (as a business) would always give priority to patients who came from outside the city over other patients. The doctor can argue that this was good for his business, but this was really frustrating for other patients. Similarly we can change the traversal logic and can have different logic for different use cases. Like filtering and sorting while traversal.

Where did they mess up?

Since the staff had the responsibility to send the patients, they did a terrible job when they asked everyone to turn up at the same time(11 AM). They could have let the patients know an hour before their turn and asked them to come over. They (as a business) failed to utilize the full potential of being an iterator. As good as the doctor and his name was in the market, we left him for another because of this sole reason.

Anyway, enough of story!

If you have used python, you must be familiar with the range function

for number in range(1, 10, 2):
    print(number)

# output:
# 1
# 3
# 5
# 7
# 9        

Lets try to implement a similar range function in Java using the Iterator Pattern.

My goal is to create something close to the below code so that it is closer to Python;

public class Main {
    public static void main(String[] args) {
        for (int num : range(1, 10, 2)) {
            System.out.println(num);
        }
    }
}        

Iterator and Iterable Interfaces in Java

There will be a separate detailed article on these two. Till then lets define them for the sake of completion:

Iterable Interface: Part of java.lang package. Provides a way to create an Iterator for a collection, allowing it to be used with the enhanced for-loop. We need to implement iterator() method as part of this contract.

Iterator Interface: Part of java.utilpackage Provides methods to traverse a collection, retrieve elements, and optionally remove elements during iteration. We need to implement hasNext() and next() methods as a part of this contract.

The Iterator Interface gives us the essence on how the Iterator Pattern is actually implemented. The next() should return the next element after following the rules of iteration. All the logic for iteration goes in this function.

It is defined something like this

package java.util;

public interface Iterator<E> {
    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}        

Lets implement the Iterable and Iterator interfaces in the Range Class. We’ll define the start, end and the step variables.

public class Range implements Iterable<Integer>, Iterator<Integer> {
    private final int start;
    private final int end;
    private final int step;
    private int current;

    public Range(int start, int end, int step) {
        if (step == 0) {
            throw new IllegalArgumentException("Step cannot be zero");
        }
        if (end > 0 && step < 0){
            throw new IllegalArgumentException("Step and End are not aligned");
        }
        this.start = start;
        this.end = end;
        this.step = step;
        this.current = start;
    }

    public Range(int end){
        this(0, end, end > 0 ? 1 : -1);
    }

    public Range(int end, int step){
        this(0, end, step);
    }

    @Override
    public boolean hasNext() {
        if (step > 0) {
            return current < end;
        } else {
            return current > end;
        }
    }

    @Override
    public Integer next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        int value = current;
        current += step;
        return value;
    }

    @Override
    public Iterator<Integer> iterator() {
        // Reset the current position for new iterations
        current = start;
        return this;
    }
}        

Since we want to use some sort of a function, we can either encapsulate the creation of the object inside a utility function or define static functions somewhere in any other class.

public class RangeCreator{
    public static Iterable<Integer> range(int end) {
        return new Range(0, end, 1);
    }
    
    public static Iterable<Integer> range(int start, int end) {
        return new Range(start, end, 1);
    }
    
    public static Iterable<Integer> range(int start, int end, int step) {
        return new Range(start, end, step);
    }
}        

USAGE:

public class Main {    
    public static void main(String[] args) {
        // Test the range method with different parameters
        System.out.println("range(10):");
        for (int num : RangeCreator.range(10)) {
            System.out.println(num);
        }

        System.out.println("range(5, 15):");
        for (int num : RangeCreator.range(5, 15)) {
            System.out.println(num);
        }

        System.out.println("range(0, 10, 2):");
        for (int num : RangeCreator.range(0, 10, 2)) {
            System.out.println(num);
        }

        System.out.println("range(10, 0, -2):");
        for (int num : RangeCreator.range(10, 0, -2)) {
            System.out.println(num);
        }
    }
}        

NOTE:

  1. The hasNext() function is not mandatory as we can have the object iterated upon store if it is the last element in the list or not.
  2. remove() and forEachRemaining() are also not mandatory for a barebones iterator pattern interface.

Thank you

要查看或添加评论,请登录

Chirag Vaswani的更多文章

  • Command Pattern

    Command Pattern

    The Command design pattern encapsulates a request as an object, thereby allowing users to parameterize clients with…

  • Chain of Responsibility Pattern

    Chain of Responsibility Pattern

    Ever faced challenges managing complex workflows with multiple handlers in a serial manner? CoR to the rescue… THE WHAT…

  • Proxy Pattern

    Proxy Pattern

    Ever wondered how you can manage heavy resources in your applications without slowing down performance? There is way to…

  • Flyweight Pattern

    Flyweight Pattern

    Flyweight is a class in boxing which includes fighters weighing up to and including 51 kg (112 lb) for a title fight…

  • Facade Pattern

    Facade Pattern

    The What The Facade Pattern is a structural design pattern that provides a simplified interface to a complex subsystem,…

  • Decorator Pattern

    Decorator Pattern

    The Decorator Pattern is a structural design pattern that allows you to dynamically attach additional responsibilities…

  • Composite Pattern

    Composite Pattern

    Have you ever struggled with managing complex hierarchical structures in your code? Composite pattern can simplify your…

  • Bridge Pattern

    Bridge Pattern

    Ever felt overwhelmed by the explosion of subclasses when trying to support multiple variations in your code? What if…

  • Adaptor Pattern

    Adaptor Pattern

    The Adapter Pattern is about creating an intermediary abstraction that translates, or adapts, one interface into…

  • Singleton Pattern: A Comprehensive Guide

    Singleton Pattern: A Comprehensive Guide

    The Singleton Pattern is one of the most widely used design patterns in software engineering. Whether you are working…

社区洞察

其他会员也浏览了