Design Patterns: Iterator
• • 12 min readThere are 23 classic design patterns, which are described in the original book, Design Patterns: Elements of Reusable Object-Oriented Software
. These patterns provide solutions to particular problems, often repeated in the software development.
In this article, I am going to describe what the Iterator Pattern is; and how and when it should be applied.
Iterator Pattern: Basic Idea
In object-oriented programming, the iterator pattern is a design pattern in which an iterator is used to traverse a container and access the container's elements. The iterator pattern decouples algorithms from containers; in some cases, algorithms are necessarily container-specific and thus cannot be decoupled. — Wikipedia
Provide a way to access the elements of an aggregate object sequentially
without exposing its underlying representation. - Design Patterns: Elements of Reusable Object-Oriented Software
The main feature of this pattern is that it lets you traverse elements of a collection without exposing its underlying representation (array, map, tree, etc.). Therefore, these are two problems that this pattern resolves:
- Decoupling of algorithms and collections.
- Allows us to change the internal implementation of a collection with no change in the algorithm's implementation.
- Allows us to add new algorithms which work all existing collection types.
To sum up, the iterator pattern hides the internal implementation of a collection from the client. The UML diagram of this pattern is the following one:
The Iterator
class is an interface witch defines the different operations to navegate through to the collection (next
or hasNext
) while that Aggregate
class will create the Iterator
. Finally, the system will use the ConcreteAggregate
and ConcreteIterator
.
Iterator Pattern: When To Use
- Your collection has a complex data structure under the hood, but you want to hide its complexity from clients.
- You need to reduce duplication of traversal code across your app.
- You want your code to be able to traverse different data structures.
Iterator Pattern: Advantages
The Iterator Pattern has several advantages, summarised in the following points:
- The code is easier to use, understand and test since the iterator uses the Single Responsibility and Open/Closed SOLID principles.
- The Single Responsibility Principle allows us to clean up the client and collections of the traversal algorithms.
- The Open/Closed Principle allows implementation of new types of collections and iterators without breaking anything.
- Parallel iteration over the same collection because each iterator object contains its own iteration state.
- Clean code because the client/context does not use a complex interface and the system is more flexible and reusable.
Iterator pattern - Example 1: Words Collection
I will now show you how you can implement this pattern using JavaScript/TypeScript. In our case, I have made up a problem in which there is a class named WordsCollection
which defines a word's list (items
) and its set of methods to get and add (getItems
and addItem
). This class is used by the client
using control structures, such as for
or forEach
. The following UML diagram shows the scenario that I have just described.
The WordsCollection
code associate is the following ones:
export class WordsCollection {
private items: string[] = [];
public getItems(): string[] {
return this.items;
}
public addItem(item: string): void {
this.items.push(item);
}
}
The client
code associate is the following ones:
import { WordsCollection } from './words-collection';
const collection = new WordsCollection();
collection.addItem('First');
collection.addItem('Second');
collection.addItem('Third');
const items = collection.getItems();
const elements = items.length;
console.log('Straight traversal:');
for (let i = 0; i <= elements; i++) {
console.log(items[i]);
}
console.log('');
console.log('Reverse traversal:');
for (let i = elements; i > 0; i--) {
console.log(items[i]);
}
The main problem in this solution is that the code is coupled. Meaning that, the client needs to known how is the internal structure of the collection to implement the two traversed methods (Straight
and Reverse
). Imagine that you need change the data structure from Array
to Map
then the code associated to the client is breaking due to the coupling. Other interesting use case of Iterator pattern is when you need a new way to iterate the collection, for example, AlphabeticalOrdered.
The solution is to use an iterator pattern and the new UML diagram using this pattern is shown below:
Therefore, the solution consists of an interface class (Iterator
) which defines the method to traverse the collection:
- current(): T.
- key(): number.
- hasMoreElements(): boolean.
- rewind: void.
export interface Iterator<T> {
// Return the current element.
current(): T;
// Return the current element and move forward to next element.
next(): T;
// Return the key of the current element.
key(): number;
// Checks if current position is valid.
hasMoreElements(): boolean;
// Rewind the Iterator to the first element.
rewind(): void;
}
The class AlphabeticalOrderIterator
is the iterator which is responsible of implementing the methods to traverse the collection in the correct way. The iterator needs the (WordsCollection
) collection using aggregation and the way to iterate (reverse or straight). So, the code associated to the AlphabeticalOrderIterator
is the following one:
import { Iterator } from './iterator.interface';
import { WordsCollection } from './words-collection';
export class AlphabeticalOrderIterator implements Iterator<string> {
private collection: WordsCollection;
private position: number = 0;
private reverse: boolean = false;
constructor(collection: WordsCollection, reverse: boolean = false) {
this.collection = collection;
this.reverse = reverse;
if (reverse) {
this.position = collection.getCount() - 1;
}
}
public rewind() {
this.position = this.reverse ? this.collection.getCount() - 1 : 0;
}
public current(): string {
return this.collection.getItems()[this.position];
}
public key(): number {
return this.position;
}
public next(): string {
const item = this.collection.getItems()[this.position];
this.position += this.reverse ? -1 : 1;
return item;
}
public hasMoreElements(): boolean {
if (this.reverse) {
return this.position < 0;
}
return this.position >= this.collection.getCount();
}
}
The next step consists of defining the Aggregator
interface and the modification of the collection to implement this interface. So, the code associated to the Aggregator
is the following one:
import { Iterator } from './iterator.interface';
export interface Aggregator {
getIterator(): Iterator<string>;
getReverseIterator(): Iterator<string>;
}
Note that the Aggregator
interface defines the methods to create the new iterators. In this problem, we need two iterators: Straight and Reverse. So, the WordsCollection
collection is modified to include these methods, as you can see in the following code:
import { Aggregator } from './agregator.interface';
import { AlphabeticalOrderIterator } from './alphabetical-order-iterator';
import { Iterator } from './iterator.interface';
export class WordsCollection implements Aggregator {
private items: string[] = [];
public getItems(): string[] {
return this.items;
}
public getCount(): number {
return this.items.length;
}
public addItem(item: string): void {
this.items.push(item);
}
public getIterator(): Iterator<string> {
return new AlphabeticalOrderIterator(this);
}
public getReverseIterator(): Iterator<string> {
return new AlphabeticalOrderIterator(this, true);
}
}
Finally, we can use the iterators in our client code, which is now decoupled, as you can see in the following code:
import { WordsCollection } from './words-collection';
const collection = new WordsCollection();
collection.addItem('First');
collection.addItem('Second');
collection.addItem('Third');
const iterator = collection.getIterator();
console.log('Straight traversal:');
while (!iterator.hasMoreElements()) {
console.log(iterator.next());
}
console.log('');
console.log('Reverse traversal:');
const reverseIterator = collection.getReverseIterator();
while (!reverseIterator.hasMoreElements()) {
console.log(reverseIterator.next());
}
The client is decoupled from the internal structure of the WordsCollection class
(Single Responsibility) and you can extend the software implementing new iterators (Open/Closed).
I have created several npm scripts that run the code's examples shown here after applying the Iterator pattern.
npm run example1-problem
npm run example1-iterator-solution-1
Iterator pattern - Example 2: Dev.To and Medium Social Networks!
Imagine that we have to create a software that allows us to send emails to our contacts in social networks, taking into account that we are going to differentiate the type of mail to send. In our network of contacts we have two categories of contacts: Friends and Coworkers. The email to be sent will be more formal depending on the type of contact to which the email will be sent.
At first we have contacts from two famous social networks: Dev.to and Medium (we do not have to clarify which is my favorite, you all know it! :-)). The implementation of the data structure of each of the social networks is different, since in Dev.to an array is used to maintain the contacts while in Medium a Map is used.
The iterator pattern will allow us to have the code completely decoupled from our contacts and social networks, allowing us to abstract ourselves from the internal implementations of each social network, and even having the ability to add new social networks (Although... Do other social networks even exist for us geeks? :P).
You can find a gif below showing the client using our entire structure (I have done a small CLI example).
In the following UML diagram you can see the solution proposed for this problem:
Alright, the model in this problem isn't a String, but rather a user's profile, as you can see in the following code:
interface IProfile {
email: string;
name: string;
contacts: string[];
}
export class Profile {
private _name: string;
private _email: string;
private _contacts: Map<string, Array<string>> = new Map();
constructor({ email, name, contacts }: IProfile) {
this._email = email;
this._name = name;
for (const contact of contacts) {
const [contactType, contactEmail] = contact.split(':');
if (!this._contacts.has(contactType)) {
this._contacts.set(contactType, []);
}
this._contacts.get(contactType).push(contactEmail);
}
}
get email(): string {
return this._email;
}
get name(): string {
return this._name;
}
public getContactsByType(contactType: string): string[] {
if (!this._contacts.has(contactType)) {
this._contacts.set(contactType, []);
}
return this._contacts.get(contactType);
}
}
In the Profile
class we have a getContactsByType
method, which returns either the friend's or coworker's contacts.
The next step is defining the iterator interface (ProfileIterator
) and the aggregator interface (SocialNetwork
) which defines the methods that must be implemented by each Iterator and Aggregator.
Therefore, the code associated to these interfaces is the following:
import { Profile } from './profile.class';
export interface ProfileIterator {
hasNext(): boolean;
getNext(): Profile;
reset(): void;
}
import { ProfileIterator } from './profile-iterator.interface';
export interface SocialNetwork {
createFriendsIterator(profileEmail: string): ProfileIterator;
createCoworkersIterator(profileEmail: string): ProfileIterator;
}
Now, we need to implement the concrete implementation of the previous interfaces to resolve our problem. The first social network that we will resolve will be Dev.to. The implementation of the aggregator and the iterator are shown below.
import { DevToIterator } from './dev-to-iterator';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
import { SocialNetwork } from '../social-network.interface';
export class DevTo implements SocialNetwork {
private contacts: Array<Profile>;
constructor(cache: Array<Profile>) {
this.contacts = cache || [];
}
public requestContactInfoFromDevToInAPI(profileEmail: string): Profile {
// Here would be a POST request to one of the DevTo API endpoints.
console.log(`DevTo: Loading profile ${profileEmail} over the network...`);
// ...and return test data.
return this.findContact(profileEmail);
}
public requestRelatedContactsFromDevToInAPI(
profileEmail: string,
contactType: string
): Array<string> {
// Here would be a POST request to one of the DevTo API endpoints.
console.log(
`DevTo: Loading ${contactType} list of ${profileEmail} over the network...`
);
// ...and return test data.
const profile = this.findContact(profileEmail);
return profile ? profile.getContactsByType(contactType) : null;
}
private findContact(profileEmail: string): Profile {
return this.contacts.find(profile => profile.email === profileEmail);
}
public createFriendsIterator(profileEmail: string): ProfileIterator {
return new DevToIterator(this, 'friends', profileEmail);
}
public createCoworkersIterator(profileEmail: string): ProfileIterator {
return new DevToIterator(this, 'coworkers', profileEmail);
}
}
Note that the collection where the contacts are stored is an Array and that the createFriendsIterator
and createCoworkersIterator
are implemented. It has several methods simulating the connection to a remote API to obtain the contacts.
The code associated to DevToIterator
class is the following one:
import { DevTo } from './dev-to.class';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
export class DevToIterator implements ProfileIterator {
private devTo: DevTo;
private type: string;
private email: string;
private currentPosition = 0;
private emails: string[] = [];
private contacts: Profile[] = [];
public constructor(devTo: DevTo, type: string, email: string) {
this.devTo = devTo;
this.type = type;
this.email = email;
}
private lazyLoad(): void {
if (this.emails.length === 0) {
const profiles = this.devTo.requestRelatedContactsFromDevToInAPI(
this.email,
this.type
);
for (const profile of profiles) {
this.emails.push(profile);
this.contacts.push(null);
}
}
}
public hasNext(): boolean {
this.lazyLoad();
return this.currentPosition < this.emails.length;
}
public getNext(): Profile {
if (!this.hasNext()) {
return null;
}
const friendEmail = this.emails[this.currentPosition];
let friendContact = this.contacts[this.currentPosition];
if (!friendContact) {
friendContact = this.devTo.requestContactInfoFromDevToInAPI(friendEmail);
this.contacts.splice(this.currentPosition, 1, friendContact);
}
this.currentPosition++;
return friendContact;
}
public reset(): void {
this.currentPosition = 0;
}
}
The most important part of the previous code is the interface implementation. The concrete implementation is based on the internal data structure of the collection (Array). You may note that I've developed a lazy method to request the contacts (think about this carefully. Should I request all friends from a friend could result in an infinite loop).
Well, at this point we should create our SocialSpammer
class which uses only interfaces. The SocialSpammer
class is decoupled from any concrete class as you can see in the following code:
import { SocialNetwork } from './social-network.interface';
export class SocialSpammer {
public network: SocialNetwork;
public constructor(network: SocialNetwork) {
this.network = network;
}
public sendSpamToFriends(profileEmail: string, message: string): void {
console.log('\nIterating over friends...\n');
const iterator = this.network.createFriendsIterator(profileEmail);
while (iterator.hasNext()) {
const profile = iterator.getNext();
this.sendMessage(profile.email, message);
}
}
public sendSpamToCoworkers(profileEmail: string, message: string): void {
console.log('\nIterating over coworkers...\n');
const iterator = this.network.createCoworkersIterator(profileEmail);
while (iterator.hasNext()) {
const profile = iterator.getNext();
this.sendMessage(profile.email, message);
}
}
private sendMessage(email: string, message: string): void {
console.log(
"Sent message to: '" + email + "'. Message body: '" + message + "'"
);
}
}
The previous code uses the iterators depending on whether the email is to friends or coworkers.
Now, we can use the code in the following client:
const network: SocialNetwork = new DevTo(mockProfiles)
const spammer = new SocialSpammer(network);
spammer.sendSpamToFriends(
'ben@dev.to',
"Hey! This is Ben's friend Jessi. Can you do me a favor and like this post [link]?");
spammer.sendSpamToCoworkers(
'ben@dev.to',
"Hey! This is Ben's boss Jess. Ben told me you would be interested in [link].");
Now would be the time to check if we can make use of the open / closed principle by creating a new social network and its iterator, without breaking our app.
The code associated to medium
class is the following one:
import { MediumIterator } from './medium-iterator';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
import { SocialNetwork } from '../social-network.interface';
export class Medium implements SocialNetwork {
private contacts: Map<string, Profile>;
constructor(cache: Array<Profile>) {
this.contacts = new Map();
if (cache) {
for (const profile of cache) {
this.contacts.set(profile.email, profile);
}
}
}
public requestContactInfoFromMediumInAPI(profileEmail: string): Profile {
// Here would be a POST request to one of the Medium API endpoints.
console.log(`Medium: Loading profile ${profileEmail} over the network...`);
// ...and return test data.
return this.findContact(profileEmail);
}
public requestRelatedContactsFromMediumInAPI(
profileEmail: string,
contactType: string
): Array<string> {
// Here would be a POST request to one of the DevTo API endpoints.
console.log(
`Medium: Loading ${contactType} list of ${profileEmail} over the network...`
);
// ...and return test data.
const profile = this.findContact(profileEmail);
return profile ? profile.getContactsByType(contactType) : null;
}
private findContact(profileEmail: string): Profile {
return this.contacts.get(profileEmail);
}
public createFriendsIterator(profileEmail: string): ProfileIterator {
return new MediumIterator(this, 'friends', profileEmail);
}
public createCoworkersIterator(profileEmail: string): ProfileIterator {
return new MediumIterator(this, 'coworkers', profileEmail);
}
}
We could have used inheritance to simplify the code between Dev.to and Medium but in order to not extend this post we have preferred to repeat code. You can see that Medium class uses a different data structure to store the contacts.
Finally, the medium-iterator
is the following one:
import { Medium } from './medium.class';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
export class MediumIterator implements ProfileIterator {
private medium: Medium;
private type: string;
private email: string;
private currentPosition = 0;
private emails: string[] = [];
private contacts: Profile[] = [];
public constructor(medium: Medium, type: string, email: string) {
this.medium = medium;
this.type = type;
this.email = email;
}
private lazyLoad(): void {
if (this.emails.length === 0) {
const profiles = this.medium.requestRelatedContactsFromMediumInAPI(
this.email,
this.type
);
for (const profile of profiles) {
this.emails.push(profile);
this.contacts.push(null);
}
}
}
public hasNext(): boolean {
this.lazyLoad();
return this.currentPosition < this.emails.length;
}
public getNext(): Profile {
if (!this.hasNext()) {
return null;
}
const friendEmail = this.emails[this.currentPosition];
let friendContact = this.contacts[this.currentPosition];
if (!friendContact) {
friendContact = this.medium.requestContactInfoFromMediumInAPI(
friendEmail
);
this.contacts.splice(this.currentPosition, 1, friendContact);
}
this.currentPosition++;
return friendContact;
}
public reset(): void {
this.currentPosition = 0;
}
}
I have created an npm script that runs the example shown here after applying the Iterator pattern and a CLI interface.
npm run example2-iterator-solution1
Conclusion
Iterator pattern can avoid coupled code in your projects. When there are several algorithms and data structures in a collection the iterator pattern is perfectly adapted. Your code will be cleaner, since you apply two famous principles, such as Single Responsibility and Open/Closed.
The most important thing is not to implement the pattern as I have shown you, but to be able to recognise the problem which this specific pattern can resolve, and when you may or may not implement said pattern. This is crucial, since implementation will vary depending on the programming language you use.
More more more...
- Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, & Vlissides, Addison Wesley, 1995
- refactoring.guru
- The Iterator Pattern — Wikipedia.
- https://www.dofactory.com/javascript/iterator-design-pattern
- https://github.com/sohamkamani/javascript-design-patterns-for-humans#-iterator
- The GitHub branch of this post is https://github.com/Caballerog/blog/tree/master/iterator-pattern