Detect subclasses in Ruby

I recently came across a problem which involved being able to select subclasses dynamically based on user input.

Given a master class of say A and a user input string, we should be able to instantiate a new subclass of A.

If we are using Rails framework, we can use ActiveSupport#CoreExt which has monkey-patched Class with two additional methods: descendants and subclasses

The source code from ActiveSupport is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Class
  def descendants
    descendants = []
    ObjectSpace.each_object(singleton_class) do |k|
      # prepends to front of descendants
      descendants.unshift k unless k == self
    end

    descendants
  end

  def subclasses
    subclasses, chain = [], descendants
    chain.each do |k|
      subclasses << k unless chain.any? { |c| c > k }
    end
    subclasses
  end
end

Given the following class hierarchy we can use the extensions like so:

1
2
3
4
5
6
7
8
9
10
require 'active_support'
require 'active_support/core_ext'

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

puts A.descendants # => [B,C]

puts A.subclasses # => [B]

Calling ObjectSpace.each_object with a class or module argument returns a list of matching classes or modules including its subclasses.

In this case, we are passing in ObjectSpace.each_object(A.singleton_class) which returns matching classes whose singleton class is either A.singleton_class or a subclass of it:

1
2
3
4
5
6
7
ObjectSpace.each_object(A.singleton_class).to_a # => [A,B,C]

B.singleton_class.superclass # => # <Class:A>

C.singleton_class.superclass # => # <Class:B>

C.singleton_class.superclass.superclass # => # <Class:A>

Calling A.descendants returns only [B,C] as within the each_object loop it excludes A itself.

Calling A.subclasses will return only direct subclasses, namely [B]. This is because it loops through the descendants list and makes a class comparison whereby only the top level subclasses are selected:

1
2
3
4
5
6
7
B > C # => true

B > B # => false

C > C # => false

C > B # => false

ObjectSpace module is certainly a powerful introspection tool to have in your toolbelt if you need detailed analytics about the objects in your current ruby process.

Stay curious and keep hacking!

Resources