Abstract away the iteration over a collection of items.

The Iterator Pattern is used basically when you have a collection of items you wish to iterate over, but don't exactly know how the collection has been implemented as (is it a list ? is it a stack ? etc).

Thus, we simply abstract away the logic of iteration to an interface and make sure that the user's of this interface are exposed to the same set of methods for iteration, irrespective of what our collection is implemented as

Analogy....

Think of the interface as your TV remote and the methods as the buttons. No matter what circuitry logic is implemented under the hood, inside the remote, you (the user of this interface) simply need to know that which button does what and it does it !

CODE ANALOGY FUNCTION
Interface(aka iterator) TV remote Exposes the logic in the form of easy to use methods
IteratorTypes Wiring inside the TV remote Classes extending the interface and containing implementation logic
Methods Buttons on TV remote Called by the user and implemented under the hood by iterator types
Caretaker User of TV remote Just uses the methods

Eg : Browser history, depending on your browser may either be stored as a List of Strings or an Array (implemented as a stack) of strings

Thus, every time the implementation of the collection is changed, we just need to create a new Iterator Type which will handle the under-the-hood-logic of this new implementation of our collection, so users can again easily iterate over it

Resources

The source code for all prominent design patterns has been properly implemented and documented in this GitHub repostitory by me.‌

crew-guy/Design-Patterns
A collection of the design patterns, implemented in java, as originally created by the GoF - crew-guy/Design-Patterns

Project Description

Here, in this article, we will be building a simple collection of browser history URLs implemented differently (as a list and as a stack) and their corresponding iterators for handling the same

Implementing the Caretaker

Performs the state management

Type Name Function
Methods push( ) Add a new item to the collection
pop( ) Remove the last item from the collection
createIterator( ) Creates an object of the Iterator (interface) and of whatever IteratorType (child class extending the interface), to iterate over the current implementation of the collection of items
    private List<String> urls = new ArrayList<>();
    private int index = 0;

    public void push(String url){
        urls.add(url);
    }

    public String pop(){
        var lastIndex = urls.size() -1;
        var lastUrl = urls.get(lastIndex);
        urls.remove(lastUrl);

        return lastUrl;
    }

    public List<String> getUrls() {
        return urls;
    }

    public void setUrls(List<String> urls) {
        this.urls = urls;
    }

    public Iterator createIterator(){
        return new ListIterator(this);
    }

    public class ListIterator implements Iterator{
        public BrowseHistory history;

        public ListIterator(BrowseHistory history){
            this.history = history;
        }

        @Override
        public boolean hasNext() {
            return (index < history.getUrls().size());
        };

        @Override
        public String current() {
            return history.getUrls().get(index);
        }

        @Override
        public void next() {
            index++;
        }
    }
}
BrowseHistory.java

Implementing the Iterator

An interface with child classes that expose the basic methods required to iterate over a collection, whilst abstracting away the iteration logic

Type Name Function
Methods hasNext( ) Returns a boolean that specifies whether we've reached the end of the collection
current( ) Returns an item of the collection
next( ) Increments iteration counter
public interface Iterator {
    boolean hasNext();
    String current();
    void next();
}
Iterator.java

Implementing the IteratorType classes

Child classes that extend the Iterator interface

Each such class contains all the iteration logic over a particular implementation of the collection (eg : List, Stack) but abstracts away all this logic, serving up only 3 methods, as enliseted by the Iterator interface.

public class BrowseHistoryForArray {
    private String[] urls = new String[10];
    private int index = 0;
    private int count = 0;

    public void push(String url){
        urls[count] = url;
        count++;
    }

    public String pop(){
        --count;
        return urls[count];
    }

    public String[] getUrls() {
        return urls;
    }

    public void setUrls(String[] urls) {
        this.urls = urls;
    }

    public Iterator createIterator(){
        return new ArrayIterator(this);
    }


    public class ArrayIterator implements Iterator{
        public BrowseHistoryForArray history;

        public ArrayIterator(BrowseHistoryForArray history){
            this.history = history;
        }

        @Override
        public boolean hasNext() {
            return (index < history.count);
        }

        @Override
        public String current() {
            return history.urls[index];
        }

        @Override
        public void next() {
            index++;
        }
    }

}
BrowseHistoryForArray.java
public class BrowseHistoryForList {
    private List<String> urls = new ArrayList<>();
    private int index = 0;

    public void push(String url){
        urls.add(url);
    }

    public String pop(){
        var lastIndex = urls.size() -1;
        var lastUrl = urls.get(lastIndex);
        urls.remove(lastUrl);
        return lastUrl;
    }

    public List<String> getUrls(){return urls;}

    public void setUrls(List<String>urls){this.urls = urls;}

    public Iterator createIterator(){
        return new ListIterator(this);
    }


    public class ListIterator implements Iterator{
        public BrowseHistoryForList history;

        public ListIterator(BrowseHistoryForList history){
            this.history = history;
        }

        @Override
        public boolean hasNext() {
            return (index < history.getUrls().size());
        }

        @Override
        public String current() {
            return history.getUrls().get(index);
        }

        @Override
        public void next() {
            index++;
        }
    }

}
BrowseHistoryForArray.java

Using this pattern in our application

Import all the necessary classes and modules into main.java for a final showdown of our application ! I have used the BrowseHistoryForArray.java implementation here but you can go forward and try out the List implementation (BrowseHistoryForList.java) as well.

var iteratorHistory = new BrowseHistory();
        iteratorHistory.push("a");
        iteratorHistory.push("b");
        iteratorHistory.push("c");

        Iterator iterator = iteratorHistory.createIterator();
        while(iterator.hasNext()){
            System.out.println(iterator.current());
            iterator.next();
        }

What basically happens when the implmentation of collection changes :

  1. push( ), pop( ) methods in Caretaker are redefined as per new implementation
  2. createIterator( ) returns a different IteratorType, (if not existent, makes such a IteratorType) which knows how to iterator over this implementation of the collection.
  3. All changes in code are confined only to the CareTaker, and if required, IteratorType class, instead of dependent classes.

Conclusion

Congrats on unlocked the secret of how the most complex iterations are made simple by workings under the hood. You should feel pretty comfortable now developing the next level iteration functionalities in your applications. Until next time...‌

eye

References..

Whatever I have learnt and implemented here in code as well as the explanation is all from the teachings of the brilliant Mosh Hamedani and his course on Design Patterns which I would recommend you to take if you are really interested in exploring the beauty of Object Oriented Programming

The Ultimate Design Pattern Series
Ace your coding job interview. Learn to write maintainable and extensible code.