This article is lecture note from "Head First Design Patterns" by Elisabeth Freeman and Kathy Sierra and CS course from Kookmin by Inkyu Kim
Especially, this article include quite a lot of stuffs from 'refactoring.guru' an website providing great quality information about design pattern
The Iterator Pattern provides a way to access the element of an aggreate object sequentially without exposing its underlying representation.
What is iterator...?
Honestly, I used Iterator quite a lot before without knowing why I use this.
Collections are one of the most used data types in programming. Nonetheless, a collection is just a container for a group of objects.
Most collections store their elements in simple lists. However, some of them are based on stacks, trees, graphs and other complex data structures.
But no matter how a collection is structured, it must provide some way of accessing its elements so that other can use these elements. There should be a way to go through each element of the collection without accessing the same elements over and over.
This may sound like an easy job if you have a collection based on a list. You just loop over all of the elements. But how do you sequentially traverse elements of a complex data structure, such as a tree? For example, one day you might be just fine with DFS of a tree. Yet the next day you might require BFS of a tree. And the next week, you might need something else, like random access to the tree elements.
Adding more and more traversal algorithms to the collection gradually blurs its primary responsibility, which is efficient data storage. Additionally, some algorithms might be tailored for a specific application, so including them into a generic collection class would be weird.
On the other hand, the client code that's supposed to work with various collections may not even care how they store thier elements. However, since collections all provide different ways of accessing their elements, you have no option other than to couple your code to the specific collection classes.
The main idea of the Iterator Pattern is to extract the traversal behaviour of a collection into a separate object called an iterator
.
Imagine that we know two different restaurant get merged and they prepare to open a new place. So we can have a nice breakfasts and lunchs at same place.
Let's dig into both restaurants codes.
<MenuItem.java>
public class MenuItem {
String name;
String description;
boolean vegetarian;
double price;
public MenuItem(String name,
String description,
boolean vegetarian,
double price)
{
this.name = name;
this.description = description;
this.vegetarian = vegetarian;
this.price = price;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public double getPrice() {
return price;
}
public boolean isVegetarian() {
return vegetarian;
}
public String toString() {
return (name + ", $" + price + "\n " + description);
}
}
<PancakeHouseMenu.java>
public class PancakeHouseMenu implements Menu {
ArrayList<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList<MenuItem>();
addItem("K&B's Pancake Breakfast",
"Pancakes with scrambled eggs, and toast",
true,
2.99);
addItem("Regular Pancake Breakfast",
"Pancakes with fried eggs, sausage",
false,
2.99);
addItem("Blueberry Pancakes",
"Pancakes made with fresh blueberries, and blueberry syrup",
true,
3.49);
addItem("Waffles",
"Waffles, with your choice of blueberries or strawberries",
true,
3.59);
}
public void addItem(String name, String description,
boolean vegetarian, double price)
{
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.add(menuItem);
}
public ArrayList<MenuItem> getMenuItems() {
return menuItems;
}
public Iterator<MenuItem> createIterator() {
return menuItems.iterator();
}
// other menu methods here
}
<DinerMenu.java>
public class DinerMenu implements Menu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("Vegetarian BLT",
"(Fakin') Bacon with lettuce & tomato on whole wheat", true, 2.99);
addItem("BLT",
"Bacon with lettuce & tomato on whole wheat", false, 2.99);
addItem("Soup of the day",
"Soup of the day, with a side of potato salad", false, 3.29);
addItem("Hotdog",
"A hot dog, with sauerkraut, relish, onions, topped with cheese",
false, 3.05);
addItem("Steamed Veggies and Brown Rice",
"A medly of steamed vegetables over brown rice", true, 3.99);
addItem("Pasta",
"Spaghetti with Marinara Sauce, and a slice of sourdough bread",
true, 3.89);
}
public void addItem(String name, String description,
boolean vegetarian, double price)
{
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS) {
System.err.println("Sorry, menu is full! Can't add item to menu");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems = numberOfItems + 1;
}
}
public MenuItem[] getMenuItems() {
return menuItems;
}
public Iterator<MenuItem> createIterator() {
return new DinerMenuIterator(menuItems);
//return new AlternatingDinerMenuIterator(menuItems);
}
// other menu methods here
}
If we want to print all the items on each menu, we'll need to call the getMenuItems method on both DinerMenu and PancakeHouseMenu to retreive their respective items.
Like this.
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();
List<MenuItem> breakfastItems = pancakeHouseMenu.getMenuItems();
MenuItem[] lunchItems = dinerMenu.getMenuItems();
Those methods look same but the calls returning diiferent type.
Now we try to print out the items from the PancakeHouseMenu, we'll loop through the items on the breakfastItems ArrayList. And to print out the Diner items we'll loop through The Array.
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = (MenuItem)breakfastItems.get(i);
System.out.print(menuItem.getName());
System.out.println("\t\t" + menuItem.getPrice());
System.out.println("\t" + menuItem.getDescription());
}
for (int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem = lunchItems[i];
System.out.print(menuItem.getName());
System.out.println("\t\t" + menuItem.getPrice());
System.out.println("\t" + menuItem.getDescription());
}
It was just fine because both collections are ArrayList and Array, so it was very easy to implement our loop (just using for loop). But there are various types of collection out there!
First of all, the Iterator interface declares the operations required for traversing a collection: fetching the next element, retrieving the current position, restarting iteration, etc.
Concrete Iterator implement specific algorithms for traversing a collection. The iterator object should track the traversal progress on its own. This allows several iterators to traverse the same collection independently of each other.
The IterableCollection interface declares one or multiple methods for getting iterators compatible with the collection. Note that the return type of the methods must be declared as the iterator interface so that the concrete collections can return various kinds of iterators.
ConcreteCollection return new instances of a particular concrete iterator class each time the client requests one. You might be wondering, where's the rest of the collections's code? Don't worry, it should be in the same class. It's just that these details aren't crucial to the actual pattern, so we're omitting them.
The Client works with both collections and iterators via their interfaces. This way the client isn't coupled to concrete classes, allowing you to use various collections and iterators with the same client code.
Typically, clients don't create iterators on their own, but instead get them from collections. Yet, in cetain cases, the client can create one directly: for example, when the client defines its won special iterator.
<Iterator.java>
public interface Iterator {
boolean hasNext();
MenuItem next();
}
<DinerMenuIterator.java>
import java.util.Iterator;
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0;
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
public MenuItem next() {
/*
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
*/
// or shorten to
return items[position++];
}
public boolean hasNext() {
/*
if (position >= items.length || items[position] == null) {
return false;
} else {
return true;
}
*/
// or shorten to
return items.length > position;
}
}
<Menu.java>
public interface Menu {
public Iterator createIterator();
}
<DinerMenu.java>
public class DinerMenu implements Menu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
***constructor here***
}
public void addItem(String name, String description,
boolean vegetarian, double price)
{
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if (numberOfItems >= MAX_ITEMS) {
System.err.println("Sorry, menu is full! Can't add item to menu");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems = numberOfItems + 1;
}
}
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
// other menu methods here
}
<Waitress.java>
public class Waitress {
Menu pancakeHouseMenu;
Menu dinerMenu;
public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("MENU\n----\nBREAKFAST");
printMenu(pancakeIterator);
System.out.println("\nLUNCH");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + " -- ");
System.out.println(menuItem.getDescription());
}
}
//other methods here
}
Look at the Waitress code above. We don't need to print all the items in the menus with different ways of implementations.
The overloaded printMenu()
uses the Iterator to step through the menu items and print them.
The Iterator encapsulates the details of working with a complex data structure, providing the client with several simple methods of accessing the collection elements. This way of approach is very useful when our collection has a complex data structure under the hood, but we want to hide its complexity from clients.
In addition, it helps to reduce duplication of traversal code across our app. The code of non-trivial iteration algorithms tends to be very bulky. When placecd within the business logic of an app, it may blur the responsibility of the original code and make it less maintainable. Moving the traversal code to ddesignated iterators can hep you make the code of the application more lean and clean.
To sum-up,
We can make our code by following Single Responsibility Principle. We can clean up the client code and the collections by extracting bulky traversal algorithms into separate classes.
And it's very useful based on the view of Open Closed Principle. We just need to implement new types of collections and iterators and pass them to existing code without breaking anything. We can iterate over the same collection in parallel because each iterator object contains its own iteration state. For the same reason, we can delay an iteration and continue it when needed.