pattern-design (8 Part Series)
There 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.
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:
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 which 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.
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.
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.
The WordsCollection code associate is the following ones:
The client code associate is the following ones:
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:
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:
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:
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:
Finally, we can use the iterators in our client code, which is now decoupled, as you can see in the following code:
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
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:
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:
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.
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:
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:
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:
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:
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:
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 12, 2019.