Iterator and Composite Patterns – Head First Approach

Alyaa TalaatAlyaa Talaat
6 min read

In this article, we will explore two powerful behavioral design patterns: Iterator and Composite. These patterns provide efficient ways to traverse collections and manage complex hierarchical structures. By using them, we can simplify client code while maintaining flexibility and scalability in object management.


What Are the Iterator and Composite Patterns?

Iterator Pattern

Definition: The Iterator Pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

The Iterator decouples the traversal logic from the underlying collection, allowing the client to iterate over a collection without knowing its structure.


Composite Pattern

Definition: The Composite Pattern allows you to compose objects into tree-like structures to represent part-whole hierarchies. The pattern lets clients treat individual objects and compositions of objects uniformly.

The Composite Pattern simplifies complex hierarchies by allowing uniform access to both individual objects and collections of objects.


Problem Scenario

Imagine you are developing a Menu System for a restaurant that offers multiple categories of food: breakfast, lunch, and dinner. Some menus have sub-menus. You need to provide an easy way to traverse through all menu items and treat individual menu items and sub-menus consistently.

With traditional methods, it would be challenging to handle this complexity efficiently, but the Iterator and Composite patterns provide a clean solution.


Using the Iterator Pattern

The Iterator Pattern is perfect when you need to traverse different collections without coupling the client code to the collection’s internal structure.

Step 1: Define the Iterator Interface

The iterator interface provides the methods for traversing through a collection.

public interface Iterator {
    boolean hasNext();
    Object next();
}

Step 2: Implement Concrete Iterators

Each collection implements its own concrete iterator. For example, the DinerMenuIterator traverses the Diner Menu array.

public class DinerMenuIterator implements Iterator {
    MenuItem[] items;
    int position = 0;

    public DinerMenuIterator(MenuItem[] items) {
        this.items = items;
    }

    public Object next() {
        MenuItem menuItem = items[position];
        position += 1;
        return menuItem;
    }

    public boolean hasNext() {
        if (position >= items.length || items[position] == null) {
            return false;
        } else {
            return true;
        }
    }
}

Step 3: Implement Aggregate Classes

The aggregate class (collection) holds the collection of items and provides an iterator for traversal.

public class DinerMenu {
    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", true, 2.99);
        // Other items...
    }

    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("Menu is full! Can’t add item.");
        } else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems += 1;
        }
    }

    public Iterator createIterator() {
        return new DinerMenuIterator(menuItems);
    }
}

Step 4: Client Code

The client code uses the iterator to access the items without knowing the internal structure of the collection.

public class Waitress {
    DinerMenu dinerMenu;

    public Waitress(DinerMenu dinerMenu) {
        this.dinerMenu = dinerMenu;
    }

    public void printMenu() {
        Iterator dinerIterator = dinerMenu.createIterator();
        printMenu(dinerIterator);
    }

    private void printMenu(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.println(menuItem.getName() + ", " + menuItem.getPrice());
        }
    }
}

Using the Composite Pattern

The Composite Pattern enables you to treat individual objects and compositions of objects uniformly. In the case of our menu system, this means we can treat both individual menu items and menus (with sub-menus) as the same type.

Step 1: Define the Component Class

The component class defines the default behavior for all menu components (both individual items and collections).

public abstract class MenuComponent {
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public MenuComponent getChild(int i) {
        throw new UnsupportedOperationException();
    }

    public String getName() {
        throw new UnsupportedOperationException();
    }

    public double getPrice() {
        throw new UnsupportedOperationException();
    }

    public void print() {
        throw new UnsupportedOperationException();
    }
}

Step 2: Create Leaf and Composite Classes

The leaf class represents individual items, such as menu items.

public class MenuItem extends MenuComponent {
    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 double getPrice() {
        return price;
    }

    public void print() {
        System.out.print(" " + getName());
        System.out.println(", " + getPrice());
    }
}

The composite class represents a collection of items (sub-menus).

public class Menu extends MenuComponent {
    ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
    String name;
    String description;

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    public MenuComponent getChild(int i) {
        return (MenuComponent) menuComponents.get(i);
    }

    public String getName() {
        return name;
    }

    public void print() {
        System.out.println("\n" + getName());
        System.out.println("---------------------");

        Iterator<MenuComponent> iterator = menuComponents.iterator();
        while (iterator.hasNext()) {
            MenuComponent menuComponent = (MenuComponent) iterator.next();
            menuComponent.print();
        }
    }
}

Step 3: Client Code

The client code can treat both individual items and composites uniformly.

public class Waitress {
    MenuComponent allMenus;

    public Waitress(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    public void printMenu() {
        allMenus.print();
    }
}

Combining the Iterator and Composite Patterns

When you use the Iterator Pattern with the Composite Pattern, you can traverse a complex hierarchical structure while treating individual objects and composites uniformly.

Here’s how you could implement an iterator for the composite structure:

public class CompositeIterator implements Iterator {
    Stack<Iterator<MenuComponent>> stack = new Stack<Iterator<MenuComponent>>();

    public CompositeIterator(Iterator<MenuComponent> iterator) {
        stack.push(iterator);
    }

    public MenuComponent next() {
        if (hasNext()) {
            Iterator<MenuComponent> iterator = stack.peek();
            MenuComponent component = iterator.next();
            if (component instanceof Menu) {
                stack.push(component.createIterator());
            }
            return component;
        } else {
            return null;
        }
    }

    public boolean hasNext() {
        if (stack.isEmpty()) {
            return false;
        } else {
            Iterator<MenuComponent> iterator = stack.peek();
            if (!iterator.hasNext()) {
                stack.pop();
                return hasNext();
            } else {
                return true;
            }
        }
    }
}

When to Use the Iterator and Composite Patterns

  • Iterator Pattern: Use this when you need to traverse different types of collections while hiding the underlying structure.

  • Composite Pattern: Use this when you need to treat individual objects and composite structures uniformly, especially in part-whole hierarchies.


Bullet Points

  • The Iterator Pattern decouples the iteration process from the collection's internal structure, allowing flexible traversal.

  • The Composite Pattern lets you compose objects into tree structures and treat both individual objects and composites uniformly.

  • These patterns are often used together to traverse and manage complex, hierarchical collections efficiently.


Conclusion

The Iterator and Composite patterns provide a powerful combination for managing and traversing complex object structures. By using these patterns, you can design systems that are both flexible and scalable, reducing the complexity of client code while maintaining the ability to handle various collection types and structures.


0
Subscribe to my newsletter

Read articles from Alyaa Talaat directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alyaa Talaat
Alyaa Talaat

As a continuous learner, I’m always exploring new technologies and best practices to enhance my skills in software development. I enjoy tackling complex coding challenges, whether it's optimizing performance, implementing new features, or debugging intricate issues.