tags:

views:

302

answers:

4

I can easily ascend the class hierarchy in Ruby:

String.ancestors     # [String, Enumerable, Comparable, Object, Kernel]
Enumerable.ancestors # [Enumerable]
Comparable.ancestors # [Comparable]
Object.ancestors     # [Object, Kernel]
Kernel.ancestors     # [Kernel]

Is there any way to descend the hierarchy as well? I'd like to do this

Animal.descendants      # [Dog, Cat, Human, ...]
Dog.descendants         # [Labrador, GreatDane, Airedale, ...]
Enumerable.descendants  # [String, Array, ...]

but there doesn't seem to be a descendants method.

(This question comes up because I want to find all the models in a Rails application that descend from a base class and list them; I have a controller that can work with any such model and I'd like to be able to add new models without having to modify the controller.)

A: 

Override the class method named inherited. This method would be passed the subclass when it is created which you can track.

Chandru
I like this one too. Overriding the method is marginally intrusive, but it makes the descendant method a little more efficient since you don't have to visit every class.
Douglas Squirrel
@Douglas While it is less intrusive, you will probably have to experiment to see if it meets your needs (i.e. when does Rails build the controller/model hierarchy?).
jleedev
A: 

If you have access to code before any subclass is loaded then you can use inherited method.

If you don't (which is not a case but it might be useful for anyone who found this post) you can just write:

x = {}
ObjectSpace.each_object(Class) do |klass|
     x[klass.superclass] ||= []
     x[klass.superclass].push klass
end
x[String]

Sorry if I missed the syntax but idea should be clear (I don't have access to ruby at this moment).

Maciej Piechotka
Just FYI, it’s probably not best to iterate over *everything* in `ObjectSpace`; that’s why it takes a class argument.
jleedev
Fixed in above. Thanks for noticing.
Maciej Piechotka
+2  A: 

Here is an example:

class Parent
  def self.descendants
    result = []
    ObjectSpace.each_object(Class) do |klass|
      result = result << klass if klass < self
    end
    result
  end
end

class Child < Parent
end

class GrandChild < Child
end

puts Parent.descendants
puts Child.descendants

puts Parent.descendants gives you:

GrandChild
Child

puts Child.descendants gives you:

GrandChild
Petros
That works great, thanks! I suppose visiting every class might be too slow if you're trying to shave milliseconds, but it's perfectly speedy for me.
Douglas Squirrel
+1  A: 

Ruby 1.9 (or 1.8.7) with nifty chained iterators:

#!/usr/bin/env ruby1.9

class Class
  def descendants
    ObjectSpace.each_object(Class).select { |klass| klass < self }
  end
end

Ruby pre-1.8.7:

#!/usr/bin/env ruby

class Class
  def descendants
    result = []
    ObjectSpace.each_object(Class) { |klass| result << klass if klass < self }
    result
  end
end

Use it like so:

#!/usr/bin/env ruby

p Animal.descendants
Jörg W Mittag