160

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.)

Douglas Squirrel
  • 2,304
  • 2
  • 19
  • 23

19 Answers19

166

Here is an example:

class Parent
  def self.descendants
    ObjectSpace.each_object(Class).select { |klass| klass < self }
  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
  • 8,862
  • 3
  • 39
  • 38
  • 1
    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 Mar 06 '10 at 20:01
  • 2
    `singleton_class` instead of `Class` make it much faster (see source at http://apidock.com/rails/Class/descendants) – brauliobo May 09 '15 at 00:41
  • 34
    Be careful if you might have a situation where the class is not loaded in the memory yet, `ObjectSpace` won't have it. – Edmund Lee Apr 05 '16 at 02:01
  • How could I make this work for `Object` and `BasicObject` ?, curious to know what they show up – Amol Pujari Feb 20 '17 at 07:33
  • @AmolPujari `p ObjectSpace.each_object(Class)` will print out all the classes. You can also get the descendants of any class you want by substutiting its name for `self` in the line of code in the method. – BobRodes Apr 03 '19 at 20:38
  • Is there a way to modify this approach to generate the `transitive hull` of all `klass < self` but instead of directly using self as second argument using a range of classes between `klass` and self building a chain like `klass < A < B < C < D < self` ? That is, `<` would return the transitive hull of "inherited from." – von spotz Oct 19 '20 at 15:31
77

If you use Rails >= 3, you have two options in place. Use .descendants if you want more than one level depth of children classes, or use .subclasses for the first level of child classes.

Example:

class Animal
end

class Mammal < Animal
end

class Dog < Mammal
end

class Fish < Animal
end

Animal.subclasses #=> [Mammal, Fish] 
Animal.descendants  #=> [Dog, Mammal, Fish]
dgilperez
  • 10,716
  • 8
  • 68
  • 96
  • 12
    Note that in development, if you have eager loading turned off, these methods will only return classes if they have been loaded (i.e. if they've been referenced already by the running server). – stephen.hanson Oct 11 '18 at 13:21
  • 4
    @stephen.hanson what's the safest way of guaranteeing correct results here? – Chris Edwards Jul 26 '19 at 15:57
  • 3
    In development mode, use `require_dependency` to autoload any necessary files BEFORE `.subclasses`, `.descendants` is called. To get the needed paths, use a path or a glob, such as `Rails.root.glob('pattern/to/*/file_*.rb)`. It can even be done in the initializer as explained in an answer here: https://stackoverflow.com/questions/29662518/loading-class-descendants-in-rails-development – Kalsan Aug 16 '21 at 08:40
26

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
  • 363,080
  • 75
  • 446
  • 653
  • 3
    This works for Modules too; just replace both instances of "Class" with "Module" in the code. – korinthe May 15 '11 at 16:52
  • 2
    For extra safety one should write `ObjectSpace.each_object(::Class)` - this will keep the code working when you happen to have a YourModule::Class defined. – Rene Saarsoo Oct 03 '11 at 10:42
23

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

jgburet
  • 535
  • 3
  • 15
Chandra Sekar
  • 10,683
  • 3
  • 39
  • 54
  • 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 Mar 06 '10 at 20:00
  • @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?). – Josh Lee Mar 06 '10 at 20:25
  • It's also more portable to various non-MRI ruby implementations, some of which have serious performance overhead from use of ObjectSpace. Class#inherited is ideal for implementing "auto-registration" patterns in Ruby. – John Whitley Jan 23 '13 at 22:49
  • Care to share an example? Since it's class level I guess you would have to store each class in some sort of global variable? – Noz Aug 22 '14 at 15:54
  • @Noz No, an instance variable on the class itself. But then the objects cannot be collected by GC. – Phrogz Apr 01 '15 at 23:40
15

Alternatively (updated for ruby 1.9+):

ObjectSpace.each_object(YourRootClass.singleton_class)

Ruby 1.8 compatible way:

ObjectSpace.each_object(class<<YourRootClass;self;end)

Note that this won't work for modules. Also, YourRootClass will be included in the answer. You can use Array#- or another way to remove it.

apeiros
  • 1,695
  • 14
  • 11
10

Although using ObjectSpace works, the inherited class method seems to be better suitable here inherited(subclass) Ruby documentation

Objectspace is essentially a way to access anything and everything that's currently using allocated memory, so iterating over every single one of its elements to check if it is a sublass of the Animal class isn't ideal.

In the code below, the inherited Animal class method implements a callback that will add any newly created subclass to its descendants array.

class Animal
  def self.inherited(subclass)
    @descendants = []
    @descendants << subclass
  end

  def self.descendants
    puts @descendants 
  end
end
Mateo Sossah
  • 131
  • 1
  • 7
4

I know you are asking how to do this in inheritance but you can achieve this with directly in Ruby by name-spacing the class (Class or Module)

module DarthVader
  module DarkForce
  end

  BlowUpDeathStar = Class.new(StandardError)

  class Luck
  end

  class Lea
  end
end

DarthVader.constants  # => [:DarkForce, :BlowUpDeathStar, :Luck, :Lea]

DarthVader
  .constants
  .map { |class_symbol| DarthVader.const_get(class_symbol) }
  .select { |c| !c.ancestors.include?(StandardError) && c.class != Module }
  # => [DarthVader::Luck, DarthVader::Lea]

It's much faster this way than comparing to every class in ObjectSpace like other solutions propose.

If you seriously need this in a inheritance you can do something like this:

class DarthVader
  def self.descendants
    DarthVader
      .constants
      .map { |class_symbol| DarthVader.const_get(class_symbol) }
  end

  class Luck < DarthVader
    # ...
  end

  class Lea < DarthVader
    # ...
  end

  def force
    'May the Force be with you'
  end
end

benchmarks here: http://www.eq8.eu/blogs/13-ruby-ancestors-descendants-and-other-annoying-relatives

update

in the end all you have to do is this

class DarthVader
  def self.inherited(klass)
    @descendants ||= []
    @descendants << klass
  end

  def self.descendants
    @descendants || []
  end
end

class Foo < DarthVader
end

DarthVader.descendants #=> [Foo]

thank you @saturnflyer for suggestion

equivalent8
  • 13,754
  • 8
  • 81
  • 109
3

Ruby Facets has Class#descendants,

require 'facets/class/descendants'

It also supports a generational distance parameter.

trans
  • 1,411
  • 1
  • 13
  • 13
3

A simple version that give an array of all the descendants of a class:

def descendants(klass)
  all_classes = klass.subclasses
  (all_classes + all_classes.map { |c| descendants(c) }.reject(&:empty?)).flatten
end
Dorian
  • 22,759
  • 8
  • 120
  • 116
  • 1
    This looks like a superior answer. Unfortunately it still falls prey to lazy-loading of classes. But I think they all do. – Dave Morse Apr 13 '17 at 23:20
  • @DaveMorse I ended up listing files and manually loading the constants to have them registered as descendants (and then ended up removing this whole thing :D) – Dorian Apr 13 '17 at 23:21
  • 2
    Note that `#subclasses` is from Rails ActiveSupport. – Alex D Oct 25 '18 at 20:14
3

(Rails <= 3.0 ) Alternatively you could use ActiveSupport::DescendantsTracker to do the deed. From source:

This module provides an internal implementation to track descendants which is faster than iterating through ObjectSpace.

Since it is modularize nicely, you could just 'cherry-pick' that particular module for your Ruby app.

chtrinh
  • 808
  • 1
  • 6
  • 8
2

Class#subclasses (Ruby 3.1+)

Starting from Ruby 3.1, there is a built-in method - Class#subclasses.

It returns an array of classes where the receiver is the direct superclass of the class, excluding singleton classes.

As a result, there is no more need to depend on ActiveSupport or write monkey-patches in order to use it.

class A; end
class B < A; end
class C < B; end
class D < A; end

A.subclasses        #=> [D, B]
B.subclasses        #=> [C]
C.subclasses        #=> []

Sources:

Marian13
  • 7,740
  • 2
  • 47
  • 51
  • 1
    Class#subclasses exists in Ruby 3.1. Only lists immediate children of Class. Does not include singleton classes. – dpneumo Sep 25 '22 at 04:24
1

You can require 'active_support/core_ext' and use the descendants method. Check out the doc, and give it a shot in IRB or pry. Can be used without Rails.

myconode
  • 2,518
  • 1
  • 26
  • 28
  • 1
    If you have to add active support to your Gemfile, then it's not really "without rails". It's just choosing the pieces of rails you like. – Caleb Feb 11 '15 at 22:35
  • 2
    This seems like a philosophical tangent that is not relevant to the topic here, but I think that using a Rails component does necessarily mean one is `using Rails` in a holistic sense. – myconode Feb 12 '15 at 23:52
1

Building on other answers (particularly those recommending subclasses and descendants), you may find that in Rails.env.development, things get confusing. This is due to eager loading turned off (by default) in development.

If you're fooling around in rails console, you can just name the class, and it will be loaded. From then on out, it will show up in subclasses.

In some situations, you may need to force the loading of classes in code. This is particularly true of Single Table Inheritance (STI), where your code rarely mentions the subclasses directly. I've run into one or two situations where I had to iterate all the STI subclasses ... which does not work very well in development.

Here's my hack to load just those classes, just for development:

if Rails.env.development?
  ## These are required for STI and subclasses() to eager load in development:
  require_dependency Rails.root.join('app', 'models', 'color', 'green.rb')
  require_dependency Rails.root.join('app', 'models', 'color', 'blue.rb')
  require_dependency Rails.root.join('app', 'models', 'color', 'yellow.rb')
end

After that, subclasses work as expected:

> Color.subclasses
=> [Color::Green, Color::Blue, Color::Yellow]

Note that this is not required in production, as all classes are eager loaded up front.

And yes, there's all kinds of code smell here. Take it or leave it...it allows you to leave eager loading off in development, while still exercising dynamic class manipulation. Once in prod, this has no performance impact.

David Hempy
  • 5,373
  • 2
  • 40
  • 68
1

Rails provides a subclasses method for every object, but it's not well documented, and I don't know where it's defined. It returns an array of class names as strings.

Daniel Tsadok
  • 573
  • 5
  • 10
0

Using descendants_tracker gem may help. The following example is copied from the gem's doc:

class Foo
  extend DescendantsTracker
end

class Bar < Foo
end

Foo.descendants # => [Bar]

This gem is used by the popular virtus gem, so I think it's pretty solid.

EricC
  • 1,355
  • 1
  • 20
  • 33
0

This method will return a multidimensional hash of all of an Object's descendants.

def descendants_mapper(klass)
  klass.subclasses.reduce({}){ |memo, subclass|
    memo[subclass] = descendants_mapper(subclass); memo
  }
end

{ MasterClass => descendants_mapper(MasterClass) }
jozwright
  • 99
  • 3
  • 11
0

To compute the transitive hull of an arbitrary class

def descendants(parent: Object)
     outSet = []
     lastLength = 0
     
     outSet = ObjectSpace.each_object(Class).select { |child| child < parent }
     
     return if outSet.empty?
     
     while outSet.length == last_length
       temp = []
       last_length = outSet.length()
       
       outSet.each do |parent|
        temp = ObjectSpace.each_object(Class).select { |child| child < parent }
       end
       
       outSet.concat temp
       outSet.uniq
       temp = nil
     end
     outSet
     end
   end
von spotz
  • 875
  • 7
  • 17
0

For Ruby 3.1+ Class#subclasses is available. Class#descendants is not implemented:

class A; end
class B < A; end
class C < B; end
class D < A; end

A.subclasses => [B, D]

A.descendants => NoMethodError: undefined method 'descendants' for A:Class

A.methods.grep('descendants') => []

For Ruby < 3.1 this is slightly faster than the Rails implementation:

def descendants
  ObjectSpace.each_object(singleton_class).reduce([]) do |des, k|
    des.unshift k unless k.singleton_class? || k == self
    des
  end
end

The Ruby 3.1+ #subclasses appears much faster than the descendants method given above.

dpneumo
  • 131
  • 1
  • 6
-1

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
  • 7,028
  • 6
  • 39
  • 61