Behavioral Design Patterns
Imagine you have a BookCollection class that stores a list of books. Internally, you decide to use an ArrayList. Your client code, wanting to print the names of all the books, might look like this:
public class LibraryClient {
public void printBooks(BookCollection collection) {
ArrayList<Book> books = collection.getBooks(); // Exposing the internal list!
for (int i = 0; i < books.size(); i++) {
System.out.println(books.get(i).getTitle());
}
}
}
This design has several major problems:
LibraryClient is now tightly coupled to the internal implementation of BookCollection. It knows that the collection uses an ArrayList.BookCollection has to expose its internal list, breaking its encapsulation and allowing the client to potentially modify it in unintended ways.ArrayList to a Book[] array, or a LinkedList, or a HashSet for better performance? You would have to find and change every single client that iterates over the collection.The client shouldn't have to know how the collection stores its elements, only that it wants to go through them one by one.
The Iterator is a behavioral design pattern that provides a way to access the elements of a collection sequentially without exposing its underlying representation.
The core idea is to extract the traversal logic from the collection and place it into a separate "iterator" object. This iterator object becomes the universal remote for traversing any collection. It provides a simple, standard interface (usually hasNext() and next()) that the client can use. The client asks the collection for its iterator, and then uses that iterator to walk through the elements, completely oblivious to whether the underlying structure is an array, a list, a set, or a complex tree.
hasNext() (to check if there are more elements) and next() (to retrieve the next element).Iterator interface. It keeps track of the current position while traversing a specific Concrete Aggregate.Iterator object (e.g., createIterator()).Aggregate interface. Its createIterator() method returns a new instance of a Concrete Iterator that is tailored to its specific internal data structure.To demonstrate the principle, let's build a custom iterator for our BookCollection.
// The Iterator interface
public interface IBookIterator {
boolean hasNext();
Book next();
}
// The Aggregate interface
public interface IBookCollection {
IBookIterator createIterator();
}
// Concrete Aggregate (stores books in an array)
public class BookCollection implements IBookCollection {
private Book[] books;
// ... constructor, addBook method, etc.
@Override
public IBookIterator createIterator() {
return new BookIterator(this);
}
// Getter for the iterator to use
public Book[] getBooks() { return this.books; }
}
// Concrete Iterator for the BookCollection
public class BookIterator implements IBookIterator {
private BookCollection collection;
private int index = 0;
public BookIterator(BookCollection collection) {
this.collection = collection;
}
@Override
public boolean hasNext() {
return index < collection.getBooks().length;
}
@Override
public Book next() {
if (!hasNext()) {
// Or throw NoSuchElementException
return null;
}
Book nextBook = collection.getBooks()[index];
index++;
return nextBook;
}
}
// The client is now completely decoupled from the internal representation
public class LibraryClient {
public void printBooks(IBookCollection collection) {
IBookIterator iterator = collection.createIterator();
while (iterator.hasNext()) {
Book book = iterator.next();
System.out.println(book.getTitle());
}
}
}
Now, we can change BookCollection to use a LinkedList or any other structure. As long as it provides a corresponding iterator, the LibraryClient code does not need to change at all.
Benefits:
Drawbacks:
java.util.Iterator interface and the java.lang.Iterable interface are the direct implementation of this pattern. The for-each loop (for (Book book : bookCollection)) is pure syntactic sugar that, behind the scenes, gets an iterator from the collection and uses it in a while loop, just like our example.createIterator() method on the aggregate class is a perfect example of the Factory Method pattern. It's a method for creating an object (the iterator), but it lets the concrete aggregate decide which specific iterator class to return.DepthFirstIterator, BreadthFirstIterator) to provide different ways of traversing the complex tree structure of a composite object.