1

I have a class game which contains some arrays of custom objects (dinosaurs, cacemen etc.), that are returned by different accessors, such as game.dinosaurs, game.cavemen etc.

At present, all these accessors just return the internally stored arrays. But now I'd like to add some custom iteration methods to these arrays returned by those accessors, to be able to write code such as game.dinosaurs.each_carnivore { ... } etc. similarly to each_element and each_attr iterators in LibXML::XML::Node. But the objects returned from my accessors game.dinosaurs and game.cavemen have to behave like arrays still.

How are things like that usually done in Ruby? Should I make the objects returned from my accessors to be some custom classes derived from Ruby's Array class? Or maybe should I just create a custom class with Enumerable mixed in?

I know I can use map or select externally on my collections, but I wanted to encapsulate these iterations internally that my class's users won't need to bother how to set up an iteration to select only carnivore dinosaurs from the internal array.

Edit: I'm not asking about how to use iterators or how to implement them, but how to add just some custom iterators to object which previously were just plain arrays (and still need to be).

SasQ
  • 14,009
  • 7
  • 43
  • 43
  • I think [Practical Object-Oriented Design in Ruby](http://www.amazon.com/Practical-Object-Oriented-Design-Ruby-Addison-Wesley/dp/0321721330), chapter 8, addresses your problem exactly, and though it presents some options, no solution will suit all cases. You should read the book (it's good!) and see which idea will fit your project. – Artem Shitov Aug 30 '13 at 06:45

3 Answers3

7

It depends (as always). You could use an array subclass and you you could build a custom class and use composition and delegation. Here's a simple example with an array subclass:

class DinosaurArray < Array
  def carnivores
    select { |dinosaur| dinosaur.type == :carnivore }
  end

  def herbivores
    select { |dinosaur| dinosaur.type == :herbivore }
  end

  def each_carnivore(&block)
    carnivores.each(&block)
  end

  def each_herbivore(&block)
    herbivores.each(&block)
  end
end

And here's a simple one with composition and delegation:

class DinosaurArray
  def initialize
    @array = []
  end

  def <<(dinosaur)
    @array << dinosaur
  end

  def carnivores
    @array.select { |dinosaur| dinosaur.type == :carnivore }
  end

  def herbivores
    @array.select { |dinosaur| dinosaur.type == :herbivore }
  end

  def each(&block)
    @array.each(&block)
  end

  def each_carnivore(&block)
    carnivores.each(&block)
  end

  def each_herbivore(&block)
    herbivores.each(&block)
  end
end

Both implementation can be used like this:

require 'ostruct'

dinosaurs = DinosaurArray.new
dinosaurs << OpenStruct.new(type: :carnivore, name: "Tyrannosaurus")
dinosaurs << OpenStruct.new(type: :carnivore, name: "Allosaurus")
dinosaurs << OpenStruct.new(type: :herbivore, name: "Apatosaurus")

puts "Dinosaurs:"
dinosaurs.each.with_index(1) { |dinosaur, i| puts "#{i}. #{dinosaur.name}" }
puts

But also has custom iterators:

puts "Carnivores:"
dinosaurs.each_carnivore.with_index(1) { |dinosaur, i| puts "#{i}. #{dinosaur.name}" }
puts

puts "Herbivores:"
dinosaurs.each_herbivore.with_index(1) { |dinosaur, i| puts "#{i}. #{dinosaur.name}" }

Output:

Dinosaurs:
1. Tyrannosaurus
2. Allosaurus
3. Apatosaurus

Carnivores:
1. Tyrannosaurus
2. Allosaurus

Herbivores:
1. Apatosaurus
Stefan
  • 109,145
  • 14
  • 143
  • 218
1

You can do this via using ruby blocks. Read more

Simple example here:

class Game

  def initialize
    @carnivoures = [1,2,3]
  end

  def each_carnivoures
    @carnivoures.each do |carni|
      yield carni
    end
  end

end

Game.new.each_carnivoures{ |c| p c}
Community
  • 1
  • 1
crackedmind
  • 918
  • 6
  • 14
  • Sorry, I'm not asking about how to use blocks or implement them. I'm asking about what is the best practice for adding such custom iterators to object returned from accessors, which previously returned just plain arrays and lots of code depend on it. I just want to *add* those additional iteration methods to the returned arrays somehow. I updated my question, so I hope it's more clear now. – SasQ Aug 30 '13 at 05:51
0

It also would be nice to have a possibility for chaining such filters. You can achieve this simply by wrapping select method into custom one, returning your new class instead of array. You may wrap some other methods as well, e.g. map:

class Units < Array
  def select
    self.class.new(super)
  end

  def dinosaurs
    select{ |unit| unit.kind == 'dinosaur' }
  end

  def cavemen
    select{ |unit| unit.kind == 'caveman' }
  end

  def carnivore
    select{ |unit| unit.type == 'carnivore' }
  end

  def herbivore
    select{ |unit| unit.type == 'herbivore' }
  end
end

Units.dinosaurs.carnivore
Units.cavemen.herbivore
Nick Roz
  • 3,918
  • 2
  • 36
  • 57