Iterator
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Also known as: Cursor
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Aggregate | Wave | Holds the enemies and defines ways through them (#each, |
| Internal Iterator | #each { |kind| ... } | The wave drives the loop and yields each enemy to a block. |
| External Iterator | Enumerator | The caller drives the loop with |
Ruby implementation
# The Aggregate: a wave of enemies. It defines #each and Enumerable supplies
# the rest. Callers never see how the wave stores its enemies.
class Wave
include Enumerable
TOUGHNESS = %i[goblin orc troll].freeze
def initialize(counts)
@counts = counts
end
# The internal iterator: enemies in spawn order, weakest kinds first.
# Without a block it returns an Enumerator, Ruby's external iterator.
def each
return enum_for(:each) unless block_given?
TOUGHNESS.each do |kind|
@counts.fetch(kind, 0).times { yield kind }
end
self
end
# A second way through the same wave: toughest kinds first.
def each_toughest_first
return enum_for(:each_toughest_first) unless block_given?
reverse_each { |kind| yield kind }
self
end
endIn use
wave = Wave.new(troll: 1, goblin: 2, orc: 1)
wave.to_a # => [:goblin, :goblin, :orc, :troll]
wave.each_toughest_first.to_a # => [:troll, :orc, :goblin, :goblin]
wave.tally # => {:goblin=>2, :orc=>1, :troll=>1}
spawner = wave.each # external iterator
spawner.next # => :goblin
spawner.next # => :goblin
The external iterator suits the game well: each tick, the spawner asks for next only when it’s time for another enemy.
Notes
Ruby idioms
- The pattern is built into the language. Define
#each,include Enumerable, and you get about 60 methods:map,select,tally,each_slice,lazy… return enum_for(:each) unless block_given?is the idiom that gives callers an external iterator (Enumerator) for free.- Several traversals, one aggregate. Any method that yields can be a traversal, like
#each_toughest_firsthere. Callers never see how the wave stores its counts.
Reach for it when
- A class holds a collection. Making it
Enumerableis almost always worth the three lines. - Several places repeat the same walk over the same data. Write the walk once, as a method that yields.
Watch for
- Exposing the inner hash with
attr_reader :countslets callers change it. Yield elements instead.
In Ruby, you can easily make any object iterable by defining
#eachand includingEnumerable. Call#eachwithout a block and you get an external iterator too.