The Iterator Design Pattern in Kotlin: Simplified and Explained

Table of Contents

When working with collections or data structures in Kotlin (or any programming language), iterating through elements is a common task. But what if you need greater control over how you traverse a collection? This is where the Iterator Design Pattern comes into play. In this article, we’ll delve into the concept of the Iterator Design Pattern, its practical implementation in Kotlin, and break it down step by step for better understanding.

Iterator Design Pattern

To iterate simply means to repeat an action. In software, iteration can be achieved using either recursion or loop structures, like for and while loops. When we need to provide functionality for iteration in a class, we often use something called an iterator.

Now, let’s talk about aggregates. Think of an aggregate as a collection of objects. It could be implemented in various forms, such as an array, a vector, or even a binary tree — essentially, any structure that holds multiple objects.

The iterator design pattern offers a structured way to handle how aggregates and their iterators are implemented. This pattern is based on two key design principles:

Separation of Concerns
This principle encourages us to keep different functionalities in separate areas. In the context of iterators, it means splitting the responsibility:

  • The aggregate focuses solely on managing (Means storing and organizing) its collection of objects.
  • The iterator takes care of traversing through the aggregate.

By doing this, we ensure that the code for maintaining the collection is cleanly separated from the code that deals with traversing it.

Decoupling of Data and Operations
This principle, rooted in generic programming, emphasizes independence between data structures and the operations performed on them. In short, the iterator pattern allows us to create traversal logic that works independently of the underlying data structure — whether it’s an array, a tree, or something else. This makes the traversal code more reusable and adaptable.

In practice, this design pattern simplifies things by moving the traversal logic out of the aggregate and into a dedicated iterator. This way, the aggregate focuses on its core responsibility — managing data — while the iterator focuses on efficiently navigating through that data. By adhering to these principles, we get cleaner, more modular, and reusable code.

Structure of the Iterator Design Pattern

Basically, here:

  • Iterator: Defines an interface for accessing and traversing elements.
  • Concrete Iterator: Implements the Iterator interface and provides the mechanism for iteration.
  • Aggregate: Represents the collection of elements.
  • Concrete Aggregate: Implements the collection (Aggregate) interface and returns an iterator to traverse its elements.

Now, let’s implement the Iterator Pattern in Kotlin

Iterator Interface

Kotlin
interface Iterator<T> {
    fun first(): T
    fun next(): T
    fun isDone(): Boolean
    fun currentItem(): T
}

Defines the standard methods First(), Next(), IsDone(), and CurrentItem().

ConcreteIterator

Implements these methods and provides specific logic for iterating over a list of items.

Kotlin
class ConcreteIterator<T>(private val items: List<T>) : Iterator<T> {
    private var currentIndex = 0

    override fun first(): T {
        return items[0]  // Return the first item
    }

    override fun next(): T {
        if (!isDone()) {
            return items[currentIndex++]  // Move to next and return the current item
        }
        throw NoSuchElementException("No more items.")
    }

    override fun isDone(): Boolean {
        return currentIndex >= items.size  // Check if we've iterated past the last item
    }

    override fun currentItem(): T {
        if (isDone()) throw NoSuchElementException("No more items.")
        return items[currentIndex]  // Return the current item
    }
}

Here, 

  • first(): Returns the first item in the list.
  • next(): Returns the next item and increments the index.
  • isDone(): Checks if all items have been traversed.
  • currentItem(): Returns the current item.

Aggregate Interface

Kotlin
interface Aggregate<T> {
    fun createIterator(): Iterator<T>
}

The Aggregate interface only defines the createIterator() method that will return an iterator.

ConcreteAggregate

Kotlin
class ConcreteAggregate<T>(private val items: List<T>) : Aggregate<T> {
    override fun createIterator(): Iterator<T> {
        return ConcreteIterator(items)  // Return a new ConcreteIterator
    }
}

The ConcreteAggregate class implements Aggregate, and its createIterator() method returns a new instance of ConcreteIterator to iterate over the collection.

Client Code

The client creates an aggregate and uses the iterator to traverse the items in the collection.

Kotlin
fun main() {
    val books = listOf("Let Us C", "Mastering Kotlin", "Wings of Fire", "Life Lessons")
    
    val bookCollection = ConcreteAggregate(books)
    val iterator = bookCollection.createIterator()

    println("First item: ${iterator.first()}")
    
    while (!iterator.isDone()) {
        println("Current item: ${iterator.currentItem()}")
        iterator.next()
    }
}

Output

Kotlin
First item: Let Us C
Current item: Let Us C
Current item: Mastering Kotlin
Current item: Wings of Fire
Current item: Life Lessons

Real-World Use Case

Let’s implement a real-world example of iterating through a collection of books in a library. 📚 It’s just an extension with a few modifications, but it’s more relatable. So, stay with me until the iteration ends. 😊

Define the Iterator Interface

The Iterator interface defines the contract for iterating through a collection.

Kotlin
interface Iterator<T> {
    fun hasNext(): Boolean // Checks if there's a next element
    fun next(): T          // Returns the next element
}

Create the Aggregate Interface

The Aggregate interface represents a collection that can return an iterator.

Kotlin
interface Aggregate<T> {
    fun createIterator(): Iterator<T>
}

Create the Concrete Aggregate

Now, let’s define a Library class that holds a collection of books.

Kotlin
data class Book(val title: String, val author: String)

class Library(private val books: List<Book>) : Aggregate<Book> {
    override fun createIterator(): Iterator<Book> = BookIterator(books)
}

Implement the Concrete Iterator

The BookIterator will traverse the Library.

Kotlin
class BookIterator(private val books: List<Book>) : Iterator<Book> {
    private var index = 0 // Keeps track of the current position
    
    override fun hasNext(): Boolean {
        // Returns true if there are more books to iterate over
        return index < books.size
    }
    
    override fun next(): Book {
        // Returns the current book and moves to the next one
        if (!hasNext()) throw NoSuchElementException("No more books in the library!")
        return books[index++]
    }
}

Bringing It All Together

Let’s use the Library and BookIterator to see the pattern in action.

Kotlin
fun main() {
    // Creating a list of books
    val books = listOf(
        Book("Let Us C", "Yashavant Kanetkar"),
        Book("Mastering Kotlin", "Naveen Tamrakar"),
        Book("Wings of Fire", "A.P.J. Abdul Kalam"),
        Book("Life Lessons", "Gaur Gopal Das")
    )

    // Creating a Library
    val library = Library(books)

    // Getting an iterator for the library
    val iterator = library.createIterator()

    // Traversing the library
    println("Books in the Library:")
    while (iterator.hasNext()) {
        val book = iterator.next()
        println("${book.title} by ${book.author}")
    }
}

Output

Kotlin
Books in the Library:
Let Us C by Yashavant Kanetkar
Mastering Kotlin by Naveen Tamrakar
Wings of Fire by A.P.J. Abdul Kalam
Life Lessons by Gaur Gopal Das

Adding a Reverse Iterator

Let’s add a ReverseBookIterator to iterate through the books in reverse order. While we could use method names like hasPrevious() or prev(), we opted to avoid them to maintain simplicity and consistency in the code.

Kotlin
class ReverseBookIterator(private val books: List<Book>) : Iterator<Book> {
    private var index = books.size - 1 // Start from the last book

    override fun hasNext(): Boolean {
        return index >= 0
    }

    override fun next(): Book {
        if (!hasNext()) throw NoSuchElementException("No more books in reverse order!")
        return books[index--]
    }
}

Modify the Library class to provide this reverse iterator.

Kotlin
fun createReverseIterator(): Iterator<Book> = ReverseBookIterator(books)

Now you can iterate in reverse.

Kotlin
val reverseIterator = library.createReverseIterator()
println("\nBooks in Reverse Order:")
while (reverseIterator.hasNext()) {
    val book = reverseIterator.next()
    println("${book.title} by ${book.author}")
}

You might be asking, “Why not just use a regular for loop or Kotlin’s built-in iterators?” Well, that’s a great question! Let me explain why the Iterator pattern could be a better fit:

  1. Custom Traversal Logic: With the Iterator pattern, you can easily implement custom traversal logic, like iterating in reverse order. This gives you more control compared to a basic for loop.
  2. Abstraction: By using an iterator, you hide the internal structure of your collection. This means the client code doesn’t need to worry about how the data is stored or how it’s being accessed.
  3. Flexibility: The Iterator pattern allows you to swap out different iterators without modifying the client code. This makes your solution more adaptable to changes in the future.

So, while a simple for loop might seem like a quick solution, using the Iterator pattern provides more flexibility, control, and abstraction in your code.

Kotlin’s Built-in Iterators

In real-world scenarios, you might not always need to implement your own iterators. Kotlin provides robust support for iterators out of the box through collections like List, Set, and Map.

Kotlin
val books = listOf(
    Book("Let Us C", "Yashavant Kanetkar"),
        Book("Mastering Kotlin", "Naveen Tamrakar"),
        Book("Wings of Fire", "A.P.J. Abdul Kalam"),
        Book("Life Lessons", "Gaur Gopal Das")
)

for (book in books) {
    println("${book.title} by ${book.author}")
}

Kotlin’s built-in iterators are efficient and follow the same principles as the Iterator pattern.

Best Practices for Using the Iterator Pattern in Kotlin

  • Leverage Kotlin’s Built-In Iterators: Kotlin’s collections (List, Set, Map) come with built-in iterators like forEach, iterator(), and more. Use the pattern when custom traversal logic is required.
  • Favor Readability: Ensure your implementation is easy to understand, especially when designing iterators for complex collections.

Advantages of the Iterator Pattern

  1. Decouples Collection and Traversal: With the Iterator pattern, the collection doesn’t need to know how its elements are being traversed. This separation of concerns makes the code cleaner and more maintainable.
  2. Uniform Traversal Interface: No matter what kind of collection you’re working with, the traversal process remains consistent. This gives you a unified way to access different types of collections without worrying about their internal structures.
  3. Supports Multiple Iterators: The Iterator pattern allows you to have multiple iterators working with the same collection at the same time. This means you can have different ways of iterating over the collection without them interfering with each other.

By using the Iterator pattern, you gain more flexibility, clarity, and control when working with collections..!

Conclusion

The Iterator Design Pattern isn’t about reinventing the wheel; it’s about designing systems that are flexible, reusable, and maintainable. In Kotlin, where we already have robust collections and iterator support, this pattern might seem overkill for basic use cases. But when you need custom traversal logic or want to decouple traversal from collection, this pattern becomes a game-changer.

I hope this explanation gave you a clear picture of how the Iterator pattern works.

Happy Iterating…~~~…~~~…!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!