Design Patterns in life and Ruby — gain an intuitive understanding of OO design patterns by linking them with real-life examples.
The Iterator Pattern answers this question: What’s next?
The Iterator Pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
In English, an iterator returns items from a collection one at a time until it has returned all items from the collection.
Let’s use the Iterator Pattern to build our movie collection.
Let's pretend we have subscriptions for both Netflix and Amazon Prime. Our goal is to combine all the movies on Netflix and Amazon Prime to build our movie collection.
We can ask Netflix for a list of available movies. For simplicity sake, only three movies are listed.
netflix_movies = [ { title: 'Train to Busan', year: '2016', director: 'Yeon Sang-ho', genre: 'Drama film/Disaster Film', length: '1h 58m' }, { title: 'The Big Short', year: '2015', director: 'Adam McKay', genre: 'Drama film/Comedy-drama', length: '2h 10m' }, { title: 'Zootopia', year: '2016', director: 'Byron Howard, Rich Moore, Jared Bush', genre: 'Mystery/Crime film', length: '1h 50m' } ]
We can also ask Amazon for a list of available movies.
amazon_movies = { 0 => { title: 'Mission: Impossible - Rogue Nation', year: '2015', director: 'Christopher McQuarrie', genre: 'Thriller/Actio', length: '2h 11m' }, 1 => { title: 'The Hunger Games: Mockingja', year: '2014', director: 'Francis Lawrence', genre: 'Fantasy/Science fiction film', length: '2h 3m' }, 2 => { title: 'Room', year: '2015', director: 'Lenny Abrahamson', genre: 'Drama film/Thriller', length: '1h 58m' } }
With both lists, we are ready to build our movie collection combining the two.
class MovieCollection attr_reader :netflix_movies, :amazon_movies def initialize(netflix_movies, amazon_movies) @netflix_movies = netflix_movies @amazon_movies = amazon_movies @all_movies = get_all_movies end private def get_all_movies movies = [] netflix_movies.each do |movie| movies << movie end amazon_movies.each do |movie_id, movie| movies << movie end movies end end
Notice the way we loop through netflix_movies
is different than the way we loop through amazon_movies
. That is because netflix_movies
is an array
and amazon_movies
is a hashmap
from movie_id
to movie
.
In order to loop through netflix_movies
and amazon_movies
correctly, MovieCollection
has to know netflix_movies
is an array and amazon_movies
is a hashmap.
This design is fine when there are only two movie subscriptions.
But now we want to subscribe to three more online movie subscriptions: Hulu, YouTube, and HBO. Their movie collection types are: array
, array
, and hashmap
, respectively.
Including these three new subscriptions, our MovieCollection
will look like this:
class MovieCollection attr_reader :netflix_movies, :amazon_movies, :hulu_movies, :youtube_movies, :hbo_movies attr_accessor :all_movies def initialize(netflix_movies, amazon_movies, hulu_movies, youtube_movies, hbo_movies) @netflix_movies = netflix_movies @amazon_movies = amazon_movies @hulu_movies = hulu_movies @youtube_movies = youtube_movies @hbo_movies = hbo_movies @all_movies = get_all_movies end private def get_all_movies movies = [] netflix_movies.each do |movie| movies << movie end amazon_movies.each do |movie_id, movie| movies << movie end hulu_movies.each do |movie| movies << movie end youtube_movies.each do |movie| movies << movie end hbo_movies.each do |movie_id, movie| movies << movie end movies end end
The get_all_movies
method is fragile because it has to remember collection types for all movie vendors. A developer might accidentally change amazon_movies.each do |movie_id, movie|
to amazon_movies.each do |movie|
, and then the code is broken.
The method is also unscalable. As the list of subscriptions grows, it becomes less maintainable.
Time to use Iterators
The complexity in the example comes from the fact that different movie vendors use different collection types for their movies. Some use arrays, others use hashmaps, and the get_all_movies
metod has to keep track of who uses what.
It would be great if the get_all_movies
method could ignore collection types.
After all, all it cares about is getting and adding the next movie to movies
until there are no more movies left in the current list.
Something like: while movie_iterator.has_next? { all_movies << movie_iterator.next }
is what we want.
With this in mind, we can rewrite the method to:
def get_all_movies movies = [] while netflix_movie_iterator.has_next? movies << netflix_movie_iterator.next end while amazon_movie_iterator.has_next? movies << amazon_movie_iterator.next end while hulu_movie_iterator.has_next? movies << hulu_movie_iterator.next end while youtube_movie_iterator.has_next? movies << youtube_movie_iterator.next end while hbo_movie_iterator.has_next? movies << hbo_movie_iterator.next end movies end
Obviously, it can be further simplified to:
def get_all_movies movies = [] movie_iterators = [ netflix_movie_iterator, amazon_movie_iterator, hulu_movie_iterator, youtube_movie_iterator, hbo_movie_iterator ] movie_iterators.each do |movie_iterator| while movie_iterator.has_next? movies << movie_iterator.next end end movies end
We can also pass the list of movie_iterators
into initialize
, so we can change our movie subscriptions without having to update the MovieCollection
class.
class MovieCollection attr_reader :movie_iterators attr_accessor :all_movies def initialize(movie_iterators) @movie_iterators = movie_iterators @all_movies = get_all_movies end private def get_all_movies movies = [] movie_iterators.each do |movie_iterator| while movie_iterator.has_next? movies << movie_iterator.next end end movies end end
Time to create Iterators
The only thing left is to create movie_iterators
.
Since there are two types of collections, array
and hashmap
, we need two types of iterators.
An ArrayIterator
:
class ArrayIterator attr_reader :collection attr_accessor :current_index def initialize(collection) @collection = collection @current_index = 0 end def has_next? current_index < collection.length end def next raise 'No more item left.' unless has_next? item = collection[current_index] self.current_index += 1 item end end
And a HashIterator
:
class HashIterator attr_reader :collection, :keys attr_accessor :current_index def initialize(collection) @collection = collection @keys = collection.keys @current_index = 0 end def has_next? current_index < keys.length end def next raise 'No more item left.' unless has_next? item = collection[keys[current_index]] self.current_index += 1 item end end
And we can use them like this:
The most important thing is that both ArrayIterator
and HashIterator
use the same interface:
- an iterator is created by passing a collection into
initialize
- an iterator responds to
has_next?
andnext
.
The unified iterator interface allows clients who use iterators, get_all_movies
in our case, to be ignorant about the type of the collection. Clients can iterate through a collection with has_next?
and next
without knowing the structure of the collection.
Woohoo, that’s exactly the definition of the Iterator Pattern!
Takeaways
The Iterator Pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
With our movie collection, we are ready to binge watch ? ? ?.
Next time we will talk about group chats.
Don’t forget to subscribe so you won’t miss the next post! ?
Enjoyed the article?
My best content on Software Design, Rails, and Career in Dev. Delivered weekly.
Pingback: Jamika Formosa
Thanks, Jamika! I use SiteGround. Here is the link: https://www.siteground.com/index.htm?afcode=75e2460833918298faa054a974277bf9 🙂