Design Pattern: Iterator and Movie Collections

Design Pattern: Iterator and Movie Collections

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? and next.

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.

Unsubscribe at anytime. I'll never spam you. Powered by ConvertKit

2 Comments Design Pattern: Iterator and Movie Collections

  1. Pingback: Jamika Formosa

Leave A Comment

Your email address will not be published. Required fields are marked *