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 removeTicket
or list ofTicket
and provide access toTicketIterator
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 toTicketIterator
. (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 accessTicketHolder
.