In the Design Patterns series that I am writing to illustrate the 23 design patterns of the band of four (GoF) in a friendly way I recently wrote about the Iterator pattern. This pattern has an incredible potential to decouple our data structure from the algorithms.
This pattern is widely implemented in different programming languages, for example, JAVA has the Iterable Interface.
In some languages there are high-level control structures that allow you to iterate the objects without having to create the Iterator pattern (since they provide it by default). However, it may be useful to modify this internal pattern to use the full potential of the language without the need for great verbosity, as in JAVA.
It is essential to know about the Iterator pattern and to have read the article that I wrote, since the examples that will illustrate this article come from the previous one.
Iterator is a structure that contains a pointer to the next element in the iteration.
Therefore, an iterable must be an object with a function iterator whose key is Symbol.iterator.
Furthermore, an iterator must be an object with a function named next that returns an object with the keys:
value: the current item in the iteration
done: true if the iteration has finished, false otherwise.
So, the previous code defines an iterable and an iterator which are used in the following way:
When you use a for-of loop the language is using an iterable, and creating its iterator. It keeps on calling the next() until done is true.
Arrays and TypedArrays over each element inside array.
Strings over each character.
Sets over their elements.
Maps over its key-value pairs.
Now I’m going to show you the new UML diagram which maintains the compatibility with custom iterators and implements the native ones.
First, let’s start with the client that will use the iterator natively without the need to instantiate any classes. It will perform the iteration directly from the for-of loop.
Note that the native use of iterators on a language cleaner, since it is integrated with the control structures of the programming language.
The WordsCollection code associated is the following one:
The first thing we have to observe is that we need to implement two interfaces:
Aggregator is used for custom Iterable and defines the necessary methods for the object to be iterable. Here we have to implement the constructor methods of the iterators.
Iterator is used by the language natively, and is the one that defines the function [Symbol.iterator]: ().
The keys to the native iterator are the factory functions of the native iterator, which instead of directly implementing it has been extracted in a new class to keep the code as clean as in the previous solution ( NativeAlphabeticalOrderIterator).
Finally, both the native iterator and the custom iterator extend their next() method from an abstract iterator which implements the rest of the methods. The main difference between the next() methods from the iterators are the values returned by each one of them, since in the native iterator the Iterator interface must be satisfied, which returns an IteratorResult while the CustomIterator returns the value directly.
This code could even be refactored to be used together with other design patterns such as Template-Method or Strategy. However, I think it is better not to further complicate the example with the addition of these patterns.
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 two npm scripts that run the code example shown here after applying the Iterator pattern.
npm run example1-problem
npm run example1-iterator-solution-1
The following example is described extensively in the following article, therefore I recommend that you read this article to understand this. However, just to give you an idea of what we are developing I will give you a brief description of the problem.
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. 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.
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 using CustomIterator:
The diagram associated with the solution that includes native iterators is as follows. In any case, we continue to maintain compatibility with custom iterators.
The main advantages of developing decoupled software is that, as our software grows, it is not affected by the changes. In fact, the client of our application is still the same piece of code since it is built based on interfaces and using dependency injection.
The sendSpamToFriends and sendSpamToCoworkers methods use the iterators, either custom or native.
In this method we use custom and native iterators. The creation of the native iterators has been delegated to an iterators’ factory to discern between friends or coworkers.
The social networks (dev.to or medium) must satisfy the SocialNetwork interface by creating the native iterator through the object of the Iterable type. The code associated to the social networks is the following one:
Each of the social networks (dev.to or medium) must satisfy the SocialNetwork interface by creating the native iterator through the object of the Iterable type .
Below is the code associated with the iterators. Both iterators (custom and native) extends from a parent iterator where the methods in common have been implemented. Although the Iterator interface has several methods in our example, we only need to implement the next() method.
The iterators associated to medium correspond to the same interface as those of dev.to and are shown below:
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
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.
Originally published at https://www.carloscaballero.io on June 20, 2019.