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 class Class
2 def descendants
3 descendants = []
4 ObjectSpace.each_object(singleton_class) do |k|
5 # prepends to front of descendants
6 descendants.unshift k unless k == self
7 end
8
9 descendants
10 end
11
12 def subclasses
13 subclasses, chain = [], descendants
14 chain.each do |k|
15 subclasses << k unless chain.any? { |c| c > k }
16 end
17 subclasses
18 end
19 end
Given the following class hierarchy we can use the extensions like so:
1 require 'active_support'
2 require 'active_support/core_ext'
3
4 class A; end
5 class B < A; end
6 class C < B; end
7
8 puts A.descendants # => [B,C]
9
10 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 ObjectSpace.each_object(A.singleton_class).to_a # => [A,B,C]
2
3 B.singleton_class.superclass # => # <Class:A>
4
5 C.singleton_class.superclass # => # <Class:B>
6
7 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 B > C # => true
2
3 B > B # => false
4
5 C > C # => false
6
7 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!