The Iterator is a behavioural design pattern that lets us traverse elements of a collection without exposing its underlying representation.
The idea is that we'll have a class, called an Iterator, which contains a reference to a corresponding aggregate object, and that Iterator can traverse over its aggregate to retrieve individual objects.
You can find the example code of this post, on GitHub
Conceptualizing the Problem
A collection is one of the most used concepts in programming. In the most basic sense, a collection is just a container for a group of objects.
Collections usually store their elements in a simple list. However, there are collections based on stacks, trees, graphs and other complex representations.
However, all collections must provide some way of accessing their elements. There should be some way to go through each element of the collection without accessing the same elements over and over.
This sounds easy if we have a collection based on a list. We just loop over all of the elements. But how do we sequentially traverse elements of a complex data structure, such as a tree? For example, one day we might need a depth-first traversal of a tree. The next day we might require a breadth-first traversal. And the next week, we might need something else, like random access to the tree elements.
Adding more and more traversal algorithms to the collection gradually blurs its primary responsibility, which is efficient data storage. Additionally, some algorithms might be tailored for a specific application, and including them in a generic collection class would be weird.
On the other hand, the client code is supposed to be collection agnostic, meaning that it doesn't even care how each collection stores its elements. However, since collections provide different traversal algorithms, we have no other option than to couple a client to a specific collection class.
The main idea of the Iterator pattern is to extract the traversal behaviour of a collection into separate objects called iterators.
In addition to implementing the algorithm itself, an iterator object encapsulates all of the traversal details, such as the current position and how many elements are left till the end. As a result, multiple iterators can traverse through the collection simultaneously, separate from each other.
Usually, iterators provide a single primary method for fetching elements. The client can keep using this method until the iterator traverses all of the elements.
Also, all iterators must implement the same interface. This makes the client code compatible with any collection type, or any traversal algorithm as long as there's a proper iterator. If we need a way of traversing a collection, that is not implemented by the existing iterators, we can just create a new iterator class, without having to change the collection of the client.
Structuring the Iterator Pattern
In its base implementation, the Iterator pattern has five participants:
- IIterator: The Iterator interface declares the operations required for traversing a collection, fetching the next element, retrieving the current position, and restarting the iteration among other things.
- Concrete Iterator: Concrete Iterators implement specific algorithms for traversing a collection. The iterator object should track the traversal progress on its own. This allows several iterators to traverse the same collection independently of each other.
- IIterableCollection: The Collection interface declares one or multiple methods for getting iterators compatible with the collection. Note that the return type of the methods must be declared as the iterator interface so that the concrete collections can return various kinds of iterators.
- ConcreteCollection: Concrete Collections return new instances of a particular concrete iterator class each time the client requests one. You might be wondering, where's the rest of the collection's code? Fear not, dear reader, it should be in the same class. It's just that these details aren't crucial to the actual pattern, so we are omitting them.
- Client: The Client works with both collections and iterators via their interfaces. This way the client isn't coupled to concrete classes, allowing us to use various collections and iterators with the same client code. Typically, clients don't create iterators on their own, but instead, get them from collections. Yet, in some instances, the client can create one directly; for example, when the client defines its own special iterator.
To demonstrate how the Iterator pattern works, we will talk about one of my favourite snacks in fantasy novels: Bertie Bott's Every Flavor Beans!
We want to build a collection for a group of jelly beans and have that collection create an iterator for itself. To do this, let's first define a class EveryFlavorBean
to represent a single bean:
namespace Iterator.Collection
{
public class EveryFlavorBean
{
private readonly string flavor;
public EveryFlavorBean(string flavor)
{
this.flavor = flavor;
}
public string Flavor
{
get { return flavor; }
}
}
}
Next, we need to create the IIterableCollection participant, which we'll call ICandyCollection
and the ConcreteCollection participant, which we'll call BertieBottsEveryFlavorBeanBox
. These classes represent a collection of beans.
using Iterator.Iterator;
namespace Iterator.Collection
{
public interface ICandyCollection
{
public IBeanIterator CreateIterator();
}
}
using Iterator.Iterator;
namespace Iterator.Collection
{
public class BertieBottsEveryFlavorBeanBox : ICandyCollection
{
private List<EveryFlavorBean> items = new();
public IBeanIterator CreateIterator()
{
return new BeanIterator(this);
}
public int Count
{
get { return items.Count; }
}
public void Add(params string[] beans)
{
foreach(string bean in beans)
items.Add(new EveryFlavorBean(bean));
}
public object this[int index]
{
get { return items[index]; }
set { items.Add((EveryFlavorBean)value); }
}
}
}
Now we can define our IIterator and ConcreteIterator participants.
using Iterator.Collection;
namespace Iterator.Iterator
{
public interface IBeanIterator
{
public EveryFlavorBean? First();
public EveryFlavorBean? Next();
public bool IsDone { get; }
public EveryFlavorBean CurrentBean { get; }
}
}
using Iterator.Collection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Iterator.Iterator
{
public class BeanIterator : IBeanIterator
{
private BertieBottsEveryFlavorBeanBox bertieBottsEveryFlavorBeanBox;
private int current = 0;
private int step = 1;
public BeanIterator(BertieBottsEveryFlavorBeanBox bertieBottsEveryFlavorBeanBox)
{
this.bertieBottsEveryFlavorBeanBox = bertieBottsEveryFlavorBeanBox;
}
public bool IsDone => current >= bertieBottsEveryFlavorBeanBox.Count;
public EveryFlavorBean? First()
{
current = 0;
return bertieBottsEveryFlavorBeanBox[current] as EveryFlavorBean;
}
public EveryFlavorBean? Next()
{
current += step;
if (!IsDone)
return bertieBottsEveryFlavorBeanBox[current] as EveryFlavorBean;
else
return null;
}
public EveryFlavorBean CurrentBean => bertieBottsEveryFlavorBeanBox[current] as EveryFlavorBean;
}
}
Notice that the ConcreteAggregate needs to implement methods by which we can manipulate objects within the collection, without exposing the collection itself. This is how it can fit with the Iterator design pattern.
Finally, in our Main()
method, we will create a collection of jelly beans and then iterate over them:
using Iterator.Collection;
using Iterator.Iterator;
BertieBottsEveryFlavorBeanBox beanBox = new();
beanBox.Add("Banana",
"Black Pepper",
"Blueberry",
"Booger",
"Candyfloss",
"Cherry",
"Cinnamon",
"Dirt",
"Earthworm",
"Earwax",
"Grass",
"Green Apple",
"Marshmallow",
"Rotten Egg",
"Sausage",
"Lemon",
"Soap",
"Tutti-Frutti",
"Vomit",
"Watermelon");
BeanIterator iterator = (BeanIterator)beanBox.CreateIterator();
for(EveryFlavorBean item = iterator.First();
!iterator.IsDone;
item = iterator.Next())
{
Console.WriteLine(item.Flavor);
}
If we run this application, we'll see the following output:
Pros and Cons of Iterator Pattern
✔ We can clean up the client code and the collection by extracting bulky traversal algorithms into separate classes, thus satisfying the Single Responsibility Principle | ❌ Applying the pattern can be an overkill if your app only works with simple collections. |
✔ We can implement new types of collections and iterators and pass them to existing code without breaking anything, thus satisfying the Open/Closed Principle | ❌ Using an iterator may be less efficient than going through elements of some specialized collections directly. |
✔ We can iterate over the same collection in parallel because each iterator object contains its own iteration state. | |
✔ For the same reason, we can delay an iteration and continue it when needed. |
Relations with Other Patterns
- We can use Iterators to traverse Composite trees.
- We can use the Factory Method along with the Iterator to let collection subclasses return different types of iterators that are compatible with the collections.
- We can use the Memento pattern along with the Iterator to capture the current iteration state and roll it back if necessary.
- We can use the Visitor pattern along with the Iterator to traverse a complex data structure and execute some operations over its elements.
Final Thoughts
In this article, we have discussed what is the Iterator pattern, when to use it and what are the pros and cons of using this design pattern. We also examined how the Iterator pattern relates to other classic design patterns.
The Iterator pattern provides a manner in which we can access and manipulate objects in a collection without exposing the collection itself. This pattern is incredibly common and incredibly useful, so keep it in mind; once you know what it is, you'll start seeing it everywhere.
The Iterator design pattern is helpful in many ways and is quite flexible if appropriately used. However, it's worth noting that the Iterator pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
Top comments (0)