The iterator design pattern is a behavioral design pattern. It is part of the Gang Of Four design patterns. The main intent of this design pattern is to provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
In simple language, what the above statement means is that the design pattern provides guidelines to segregate the implementation of a collection from its iteration.
If you think about this in terms of Solid Design Principles, it will make a lot of sense. Because the collection which is an aggregate of objects must not have the responsibility of how to iterate through the collection. That responsibility must remain with the iterator itself.
And that is what the iterator design pattern advocates. Now let us go through the implementation of this pattern to make it clearer.
Implementation of the iterator pattern
For the purpose of this implementation, there will be an aggregate. The aggregate is a class that will hold the collection. And then there will be an iterator. The iterator will be responsible for iterating through the aggregate or the collection.
Since the aggregate and the iterator are related to each other, we need to create both of them almost in parallel.
The Aggregate class in the iterator pattern
namespace Iterator.Demo;
internal interface IAggregate<T>
{
T this[int index] { get; set; }
int Count { get; }
IIterator<T> Iterator { get; }
}
internal class Aggregate<T> : IAggregate<T>
{
private IIterator<T> _iterator;
private List<T> _list = new List<T>();
public T this[int index]
{
get { return _list[index]; }
set { _list.Add(value); }
}
public IIterator<T> Iterator
{
get
{
if (this._iterator == null) _iterator = new Iterator<T>(this);
return _iterator;
}
}
public int Count => _list.Count;
}
For the aggregate implementation, we will create a generic interface IAggregate
. This interface will contain an indexer, a Count
property, and an Iterator
property. The return type of Count
will be an integer and the return type of the Iterator
will be IIterator
.
Inside the Aggregate
class, we will create a couple of local variables. The first one is an IIterator
and the second one will be a generic List
.
Next, we will need some way to expose the value because the iterator will need the values to iterate through. And also we will need a way to add values. We can support both through a single indexer implementation. The Get
method will return the value from the List
based on the index. And The Set
method will call the Add
method on the List
object to add the value to the.
And the Iterator
property will return a new instance of the generic iterator if it is not created already. But if the iterator object exists, it will just return the already available object.
And finally, we will need the count of the aggregate, which is the total number of items in the aggregate. Hence the Count
property will return the total count of items in the List
.
So here now the aggregate is just holding the object and the iterator will be responsible for iterating through the object.
The Iterator class in the iterator pattern
The responsibility of the iterator will be to iterate through the Aggregate
. Hence for the iteration need to perform these three tasks:
- Firstly, it will need the current value of the aggregate
- Secondly, it will need to know if there are any items are still available on the aggregate
- Thirdly, it will need a way to move to the next item on the aggregate
Hence to support these responsibilities of the iterator, we will create three members in the iterator interface.
The Interface
namespace Iterator.Demo;
internal interface IIterator<T>
{
T Next();
T Current { get; }
bool IsLeft();
}
The first member will be the Next
method, and the second member is the Current
property, which can be just a Get
property. And the last member is IsLeft
method.
Next, we will implement the Iterator class, which will implement the IIterator
interface.
The concrete class
namespace Iterator.Demo;
internal class Iterator<T> : IIterator<T>
{
private readonly IAggregate<T> _aggregate;
int index = 0;
public Iterator(IAggregate<T> aggregate)
{
this._aggregate = aggregate;
}
public T Current => _aggregate[index];
public bool IsLeft() => index < _aggregate.Count;
public T Next()
{
index++;
return IsLeft() ? _aggregate[index] : default;
}
}
The constructor of the Iterator
class will inject the IAggregate
interface, as it will need it for core functionality. And we will declare a member variable in the class to hold the index of the aggregate, and its default value will be 0.
The Current
property will return the current item, and for that, it will use the index to get the current item from the aggregate using its indexer.
For the IsLeft
property, we just need to find out if any item is left in the aggregate. And we can achieve that easily by making sure that the Count
property is greater than the current index.
Finally, for the Next
implementation, we will first increment the index
. And after that, if any item is left we will return the item from the aggregate at that index. If no item is left, we will return the default value of the generic type.
For the example, we will declare a record type for the data model. Hence for that, we will declare a Person
record type. And it will contain two properties, Name
, and Age
.
internal record Person(string Name, int Age);
Using the aggregate and iterator
Now we will update the Program
class to iterate through an aggregate.
using Iterator.Demo;
var persons = new Aggregate<Person>();
persons[0] = new Person("John", 30);
persons[1] = new Person("Jane", 20);
persons[3] = new Person("Michael", 10);
var iterator = persons.Iterator;
while (iterator.IsLeft())
{
Console.WriteLine(iterator.Current);
iterator.Next();
}
Firstly, I will create an Aggregate
of type Person
, and I will add three Person
objects to the aggregate.
Next, I will get the Iterator
from the aggregate.
And finally, through a while
loop, I will use the IsLeft
method to iterate through the aggregate using the iterator and print the current item.
If we run this application, we will see the code will iterate through the aggregate and it will print the three Person objects added to the aggregate.
Conclusion
Using the iterator pattern it is very simple to implement an extremely generic implementation. And we can use this implementation with multiple types of objects.
The C#/.NET already provides an implementation of the iterator design pattern through IEnumerator. Tough the IEnumerator implementation in C#/.NET is more sophisticated than my implementation.
This is the high-level implementation of the iterator pattern. And there are situations where you might need to create your own iterator, in which case it will be useful. Otherwise, you can just go ahead with IEnumerator and that will work for you.
A Youtube video for this implementation is available here.