Design Pattern : Iterator

Design Pattern : Iterator

Design Patterns: Elements of Reusable Object-Oriented Software

What is Iterator Design Pattern?

If get to know something new by reading my articles, don't forget to endorse me on LinkedIn

** Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.**

As a Java programmer, it’s likely you have worked with aggregate objects, which are referred as collections in Java. A collection is an object that acts as a container to store other objects, which are referred as elements. Based on how a collection stores elements, there are different types of collections. For example, some collections may allow duplicate elements while others may not.

ArrayList, LinkedList, HashMap, LinkedHashSet` are few of the collections that are part of the Java Collection Framework. Like any other objects, a collection can be passed around the application or stored in other collection objects.

Let's think in-terms of Ticket Management System

Where we can add or remove a Ticket or List of Tickets from the system. Also, we can find a Ticket or update a Ticket or travers through the Ticket list one by one.

By creating a Data Holder for Ticket Management System is enough for our requirements, where we can create a class that provides public APIs to the client but one minute. By doing that, the Data Holder’s underlying structure and implementation will be exposed to clients. This is a bad object-oriented design principle that does not follow encapsulation.

In addition, if you go back and revisit the SOLID design principles, it will be apparent that implementing element access and traversal operations in the collection itself is a clear violation of the Single Responsibility Principle. A collection should be only responsible to act as a container for storing elements, and any other responsibility such as element access and traversal should not be part of it.

The Iterator pattern addresses such recurring problems when dealing with aggregate objects. What this pattern says is that aggregate objects should provide a way to access its elements sequentially without exposing its internal structure.

Let’s summarize the participants of the Iterator Pattern.

  • Aggregate(TicketHolder): An interface that define all APIs to store or remove Ticket or list of Ticket and provide access to TicketIterator to iterate though the list of Tickets.

  • ConcreteAggregate(TicketHolderImpl): Is a concrete class that implements the Aggregate interface to create and return an iterator.

  • Iterator(TicketIterator): An interface that expose some APIs to the client to access, find, update, traverse thorough the elements.

  • ConcreteIterator(TicketIteratorImpl): Is a concrete class that implements the Iterator interface. Objects of this class keeps track of the elements and implements access and traversal operations on the elements, find an element or update an element.

  • Client(VerifyTickets): Client access point to access TicketHolder

Lets get into the coding part

data class TicketModel(val id: Int, var name: String = "")

interface TicketHolder {
    fun addTicket(ticketModel: TicketModel)
    fun addTickets(ticketModel: MutableList<TicketModel>)
    fun removeTicket(ticketModel: TicketModel)
    fun removeTickets(ticketModel: MutableList<TicketModel>)
    fun clearAll(): Boolean
    val ticketIterator: TicketIterator
}

class TicketHolderImpl : TicketHolder {
    private var ticketList: MutableList<TicketModel> = ArrayList()
    private var iterator: TicketIterator = TicketIteratorImpl(ticketList)
    override fun addTicket(ticketModel: TicketModel) {
        ticketList.add(ticketModel)
    }
    override fun addTickets(ticketModel: MutableList<TicketModel>) {
        ticketList.addAll(ticketModel)
    }
    override fun removeTicket(ticketModel: TicketModel) {
        ticketList.remove(ticketModel)
    }
    override fun removeTickets(ticketModel: MutableList<TicketModel>) {
        ticketList.removeAll(ticketModel)
    }
    override fun clearAll(): Boolean {
        this.ticketList.clear()
        return true
    }
    override val ticketIterator: TicketIterator
        get() = iterator
}

We have a Data Class (TicketModel) that represent a Ticket object, Data Holder and it's concrete implementation (TicketHolder, TicketHolderImpl) where all of the APIs are defined and access point for the TicketIterator

interface TicketIterator {
    fun nextTicket(): TicketModel
    fun findTicket(id: Int): TicketModel?
    fun updateTicket(id: Int, name: String): Boolean
    val hasNextTicket: Boolean
    fun resetPositionTo(newPosition: Int)
}

class TicketIteratorImpl(private var ticketList: List<TicketModel>) : TicketIterator {
    private var position: Int = 0
    override fun nextTicket(): TicketModel {
        val ticket = ticketList[position]
        println("Ticket Info at position : $position - $ticket")
        position++
        return ticket
    }
    override fun findTicket(id: Int): TicketModel? =
        ticketList.find { it.id == id; }?.apply {
            println("Ticket found: $this")
        } ?: kotlin.run { println("No ticket found with id :${id}"); null }
    override fun updateTicket(id: Int, name: String): Boolean {
        return ticketList.find { it.id == id }?.let {
            it.name = name
            return true
        } ?: kotlin.run { println("No ticket found with id :${id}"); return@run false }
    }
    override val hasNextTicket: Boolean
        get() = position >= ticketList.size
    override fun resetPositionTo(newPosition: Int) {
        this.position = newPosition
        println("Position reset to $newPosition")
    }
}

Now, we have the Iterator and implementation of Iterator (TicketIterator, TicketIteratorImpl) which enable access to public APIs to Iterate, Get Next Ticket, Find a Ticket, Update a Ticket.

object VerifyTickets {
    private val ticketHolder: TicketHolder = TicketHolderImpl()
    fun getTicketHolder(freshStart: Boolean = true): TicketHolder {
        if (freshStart) ticketHolder.clearAll(); ticketHolder.addTickets(provideValidBusTickets())
        return ticketHolder
    }
    fun printAll(holder: TicketHolder) {
        println()
        println("Starting to print....")
        while (!holder.ticketIterator.hasNextTicket) {
            holder.ticketIterator.nextTicket()
        }
        println("Ending of print....")
        println()
    }
    /*Return a list of valid Tickets*/
    private fun provideValidBusTickets(): MutableList<TicketModel> = mutableListOf(
        TicketModel(0).apply { name = "Something $id" },
        TicketModel(1).apply { name = "Something $id" },
        TicketModel(2).apply { name = "Something $id" },
        TicketModel(3).apply { name = "Something $id" },
        TicketModel(4).apply { name = "Something $id" },
        TicketModel(5).apply { name = "Something $id" },
        TicketModel(6).apply { name = "Something $id" },
        TicketModel(7).apply { name = "Something $id" },
        TicketModel(8).apply { name = "Something $id" },
        TicketModel(9).apply { name = "Something $id" },
        TicketModel(10).apply { name = "Something $id" },
        TicketModel(11).apply { name = "Something $id" }
    )
}

VerifyTickets is the client access point to the whole system, where we have two public APIs, getTicketHolder(freshStart: Boolean = true) to get TicketHolder instance (with some Dummy Data) and printAll(holder: TicketHolder) to print all available tickets in the system.

Let's test.

class IteratorPatternExample {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val holder = VerifyTickets.getTicketHolder()

            /*Printing several tickets*/
            println("Printing next tickets...")
            holder.ticketIterator.nextTicket()
            holder.ticketIterator.nextTicket()
            holder.ticketIterator.nextTicket()

            /*Find a specific ticket*/
            println()
            println("Finding tickets...")
            holder.ticketIterator.findTicket(10)
            holder.ticketIterator.findTicket(55)

            /*Update a specific ticket*/
            println()
            println("Update ticket...")
            println("Ticket update successfully :${holder.ticketIterator.updateTicket(1, "Updated Name")}")
            println("Ticket update successfully :${holder.ticketIterator.updateTicket(55, "Exception")}")

            /*Find a specific ticket*/
            println()
            println("Find ticket...")
            holder.ticketIterator.findTicket(1)

            /*Reset the iterator position*/
            println()
            println("Reset iterator position...")
            holder.ticketIterator.resetPositionTo(0)

            /*Print all tickets*/
            VerifyTickets.printAll(holder)
        }
    }
}

Result:

Printing next tickets...
Ticket Info at position : 0 - TicketModel(id=0, name=Something 0)
Ticket Info at position : 1 - TicketModel(id=1, name=Something 1)
Ticket Info at position : 2 - TicketModel(id=2, name=Something 2)

Finding tickets...
Ticket found: TicketModel(id=10, name=Something 10)
No ticket found with id :55

Update ticket...
Ticket update successfully :true
No ticket found with id :55
Ticket update successfully :false

Find ticket...
Ticket found: TicketModel(id=1, name=Updated Name)

Reset iterator position...
Position reset to 0

Starting to print....
Ticket Info at position : 0 - TicketModel(id=0, name=Something 0)
Ticket Info at position : 1 - TicketModel(id=1, name=Updated Name)
Ticket Info at position : 2 - TicketModel(id=2, name=Something 2)
Ticket Info at position : 3 - TicketModel(id=3, name=Something 3)
Ticket Info at position : 4 - TicketModel(id=4, name=Something 4)
Ticket Info at position : 5 - TicketModel(id=5, name=Something 5)
Ticket Info at position : 6 - TicketModel(id=6, name=Something 6)
Ticket Info at position : 7 - TicketModel(id=7, name=Something 7)
Ticket Info at position : 8 - TicketModel(id=8, name=Something 8)
Ticket Info at position : 9 - TicketModel(id=9, name=Something 9)
Ticket Info at position : 10 - TicketModel(id=10, name=Something 10)
Ticket Info at position : 11 - TicketModel(id=11, name=Something 11)
Ending of print....


Process finished with exit code 0

Summary

  • TicketHolder focus on data storing, removing and provide access to TicketIterator. (Concrete classes implements the logic)

  • TicketIterator focus on Iteration process, finding ticket, update ticket or check if next ticket available or not. (Concrete classes implements the logic)

  • VerifyTickets provides access to client to access TicketHolder.

Full Implementation in Kotlin