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

C# : How can we access private method outside class

Introduction In object-oriented programming, encapsulation is a fundamental principle that restricts direct access to the internal implementation details of a class. Private methods, being part of this internal implementation, are designed to be accessible only within the confines of the class they belong to. However, there might be scenarios where you need to access a private method from outside the class. In this blog post, we'll explore several techniques to achieve this in C#. 1. Reflection: A Powerful Yet Delicate Approach Reflection is a mechanism in C# that allows inspecting and interacting with metadata about types, fields, properties, and methods. While it provides a way to access private methods, it should be used cautiously due to its potential impact on maintainability and performance. using System ; using System . Reflection ; public class MyClass { private void PrivateMethod ( ) { Console . WriteLine ( "This is a private method."...

20+ LINQ Concepts with .Net Code

LINQ   (Language Integrated Query) is one of the most powerful features in .NET, providing a unified syntax to query collections, databases, XML, and other data sources. Below are 20+ important LINQ concepts, their explanations, and code snippets to help you understand their usage. 1.  Where  (Filtering) The  Where()  method is used to filter a collection based on a given condition. var numbers = new List < int > { 1 , 2 , 3 , 4 , 5 , 6 } ; var evenNumbers = numbers . Where ( n => n % 2 == 0 ) . ToList ( ) ; // Output: [2, 4, 6] C# Copy 2.  Select  (Projection) The  Select()  method projects each element of a sequence into a new form, allowing transformation of data. var employees = new List < Employee > { /* ... */ } ; var employeeNames = employees . Select ( e => e . Name ) . ToList ( ) ; // Output: List of employee names C# Copy 3.  OrderBy  (Sorting in Ascending Order) The  Or...

C# : Understanding Types of Classes

In C#, classes serve as the building blocks of object-oriented programming, providing a blueprint for creating objects. Understanding the types of classes and their applications is crucial for designing robust and maintainable software. In this blog, we’ll delve into various types of classes in C#, accompanied by real-world scenarios and code snippets for a practical understanding. 1. Regular (Instance) Classes Definition: Regular classes are the most common type and are used to create instances or objects. They can contain fields, properties, methods, and other members. Example Scenario: A Person class representing individual persons with properties like Name and Age. public class Person { public string Name { get ; set ; } public int Age { get ; set ; } } 2. Static Classes Definition: A static class cannot be instantiated and can only contain static members (methods, properties, fields). It’s often used for utility functions. Example Scenario: A MathUtility cla...