Understanding the Iterator Design Pattern in C#

The Iterator design pattern is a behavioral pattern that provides a way to access elements of a collection sequentially without exposing its underlying representation. This pattern is useful for traversing different data structures in a uniform way.
Understanding the Memento Design Pattern in C#
Let's consider a scenario where we have a Book
class and a BookCollection
class. We want to iterate over the BookCollection
without exposing its internal details.
Example without Iterator Design Pattern
using System;
using System.Collections.Generic;
namespace WithoutIteratorPattern
{
// Book class
class Book
{
public string Title { get; private set; }
public Book(string title)
{
Title = title;
}
}
// Concrete collection
class BookCollection
{
private readonly List<Book> _books = new List<Book>();
public int Count => _books.Count;
public Book this[int index] => _books[index];
public void AddBook(Book book)
{
_books.Add(book);
}
public List<Book> GetBooks()
{
return _books;
}
}
class Program
{
static void Main(string[] args)
{
BookCollection collection = new BookCollection();
collection.AddBook(new Book("Design Patterns"));
collection.AddBook(new Book("Clean Code"));
collection.AddBook(new Book("Refactoring"));
List<Book> books = collection.GetBooks();
Console.WriteLine("Iterating over collection:");
for (int i = 0; i < books.Count; i++)
{
Console.WriteLine(books[i].Title);
}
}
}
}
Problems in the Non-Pattern Approach
Lack of Abstraction:The client code (in
Program.Main
) directly accesses the internal representation of the collection (List<Book>
). This means any change in the collection type (e.g., fromList<Book>
to another collection type) requires changes in the client code.Limited Flexibility:If different iteration behaviors are needed (e.g., reverse iteration, skipping certain elements), the client code must be modified, making it less flexible and harder to maintain.
Code Duplication:Each time we need to iterate over the collection, we would write similar looping code, leading to code duplication and potential inconsistencies.
Encapsulation Violation:Exposing the internal list of books through
GetBooks()
method breaks encapsulation, making it possible for the client code to modify the collection directly, which can lead to unexpected behaviors.
How Iterator Pattern Solves These Problems
Encapsulation:The Iterator Pattern encapsulates the iteration logic inside the iterator. The client code interacts with the collection through the iterator interface, without knowing or depending on the underlying collection structure.
Single Responsibility:The collection class (
BookCollection
) is responsible for managing the collection of books, and the iterator class (BookIterator
) is responsible for the iteration logic. This separation of concerns makes the code more modular and easier to maintain.Flexibility:Different iteration behaviors can be implemented by creating different iterators. For example, if you need a reverse iterator, you can create a
ReverseBookIterator
without changing the client code.Consistency:The iteration logic is centralized in the iterator, reducing code duplication and ensuring consistent behavior across different parts of the application.
Revisited Code with Iterator Pattern
Here is how we can implement this pattern:
using System;
using System.Collections;
using System.Collections.Generic;
namespace IteratorPattern
{
// Book class
class Book
{
public string Title { get; private set; }
public Book(string title)
{
Title = title;
}
}
// Iterator interface
interface IIterator<T>
{
T Current { get; }
bool MoveNext();
void Reset();
}
// Concrete iterator
class BookIterator : IIterator<Book>
{
private readonly BookCollection _collection;
private int _currentIndex = -1;
public BookIterator(BookCollection collection)
{
_collection = collection;
}
public Book Current => _collection[_currentIndex];
public bool MoveNext()
{
_currentIndex++;
return _currentIndex < _collection.Count;
}
public void Reset()
{
_currentIndex = -1;
}
}
// Collection interface
interface IBookCollection
{
IIterator<Book> CreateIterator();
int Count { get; }
Book this[int index] { get; }
}
// Concrete collection
class BookCollection : IBookCollection
{
private readonly List<Book> _books = new List<Book>();
public int Count => _books.Count;
public Book this[int index] => _books[index];
public void AddBook(Book book)
{
_books.Add(book);
}
public IIterator<Book> CreateIterator()
{
return new BookIterator(this);
}
}
class Program
{
static void Main(string[] args)
{
BookCollection collection = new BookCollection();
collection.AddBook(new Book("Design Patterns"));
collection.AddBook(new Book("Clean Code"));
collection.AddBook(new Book("Refactoring"));
IIterator<Book> iterator = collection.CreateIterator();
Console.WriteLine("Iterating over collection:");
while (iterator.MoveNext())
{
Console.WriteLine(iterator.Current.Title);
}
}
}
}
Why Can't We Use Other Design Patterns Instead?
Composite Pattern: The Composite pattern is used for treating individual objects and compositions of objects uniformly. It does not provide a mechanism for traversing a collection.
Visitor Pattern: The Visitor pattern is used for performing operations on elements of an object structure without changing the classes of the elements. It is more suitable for operations that need to be applied across a collection rather than simple traversal.
Command Pattern: The Command pattern encapsulates a request as an object, allowing for parameterization and queuing of requests. It is not designed for accessing elements of a collection sequentially.
Steps to Identify Use Cases for the Iterator Pattern
Collection Traversal: Identify scenarios where you need to traverse elements of a collection sequentially.
Encapsulation Requirement: Ensure that the internal representation of the collection should be hidden from the client.
Uniform Access: Consider the Iterator pattern when you need a uniform way to traverse different types of collections.
Multiple Traversals: Use the Iterator pattern to enable multiple independent traversals of a collection.
By following these steps and implementing the Iterator pattern, you can achieve encapsulated and uniform traversal of collections, improving flexibility and separation of concerns in your system.