Skip to main content

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

  1. 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., from List<Book> to another collection type) requires changes in the client code.

  2. 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.

  3. Code Duplication:Each time we need to iterate over the collection, we would write similar looping code, leading to code duplication and potential inconsistencies.

  4. 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
  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. Collection Traversal: Identify scenarios where you need to traverse elements of a collection sequentially.
  2. Encapsulation Requirement: Ensure that the internal representation of the collection should be hidden from the client.
  3. Uniform Access: Consider the Iterator pattern when you need a uniform way to traverse different types of collections.
  4. 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.

Comments

Popular posts from this blog

Implementing and Integrating RabbitMQ in .NET Core Application: Shopping Cart and Order API

RabbitMQ is a robust message broker that enables communication between services in a decoupled, reliable manner. In this guide, we’ll implement RabbitMQ in a .NET Core application to connect two microservices: Shopping Cart API (Producer) and Order API (Consumer). 1. Prerequisites Install RabbitMQ locally or on a server. Default Management UI: http://localhost:15672 Default Credentials: guest/guest Install the RabbitMQ.Client package for .NET: dotnet add package RabbitMQ.Client 2. Architecture Overview Shopping Cart API (Producer): Sends a message when a user places an order. RabbitMQ : Acts as the broker to hold the message. Order API (Consumer): Receives the message and processes the order. 3. RabbitMQ Producer: Shopping Cart API Step 1: Install RabbitMQ.Client Ensure the RabbitMQ client library is installed: dotnet add package RabbitMQ.Client Step 2: Create the Producer Service Add a RabbitMQProducer class to send messages. RabbitMQProducer.cs : using RabbitMQ.Client; usin...

Clean Architecture: What It Is and How It Differs from Microservices

In the tech world, buzzwords like   Clean Architecture   and   Microservices   often dominate discussions about building scalable, maintainable applications. But what exactly is Clean Architecture? How does it compare to Microservices? And most importantly, is it more efficient? Let’s break it all down, from understanding the core principles of Clean Architecture to comparing it with Microservices. By the end of this blog, you’ll know when to use each and why Clean Architecture might just be the silent hero your projects need. What is Clean Architecture? Clean Architecture  is a design paradigm introduced by Robert C. Martin (Uncle Bob) in his book  Clean Architecture: A Craftsman’s Guide to Software Structure and Design . It’s an evolution of layered architecture, focusing on organizing code in a way that makes it  flexible ,  testable , and  easy to maintain . Core Principles of Clean Architecture Dependency Inversion : High-level modules s...

How Does My .NET Core Application Build Once and Run Everywhere?

One of the most powerful features of .NET Core is its cross-platform nature. Unlike the traditional .NET Framework, which was limited to Windows, .NET Core allows you to build your application once and run it on Windows , Linux , or macOS . This makes it an excellent choice for modern, scalable, and portable applications. In this blog, we’ll explore how .NET Core achieves this, the underlying architecture, and how you can leverage it to make your applications truly cross-platform. Key Features of .NET Core for Cross-Platform Development Platform Independence : .NET Core Runtime is available for multiple platforms (Windows, Linux, macOS). Applications can run seamlessly without platform-specific adjustments. Build Once, Run Anywhere : Compile your code once and deploy it on any OS with minimal effort. Self-Contained Deployment : .NET Core apps can include the runtime in the deployment package, making them independent of the host system's installed runtime. Standardized Libraries ...