Tracking Class Descendants in Ruby
I am going through all Active Support core extensions lately because I am writing the Active Support Core Extensions guide, due for Rails 3. There are some patches in master as a result of that walkthrough, and I am now focusing on keeping track of descendants in a class hierarchy.
A known technique uses ObjectSpace.each_object. That is a method that receives a class or module as argument and yields all objects that have that class or module among their parents. Since classes are instances of the class Class, you can select descendants of class C this way:
descendants_of_C = [] ObjectSpace.each_object(Class) do |klass| descendants_of_C << klass if klass < C end
That is a brute force approach, it works, but it is inefficient. JRuby even disables ObjectSpace by default for performance reasons.
A better approach is to leverage the inherited hook. Classes may optionally implement a class method inherited that is called whenever they are subclassed. The subclass is passed as argument:
class User def self.inherited(subclass) puts 0 end end class Admin < User puts 1 end # output is 0 1
That's a perfect place to keep track of descendants:
class C class << self def inherited(subclass) C.descendants << subclass super end def descendants @descendants ||= [] end end end
In that code we have an array of descendants in @descendants. That is an instance variable of the very class C. Remember classes are ordinary objects in Ruby and so they may have instance variables. It is better to use an instance variable instead of a class variable because class variables are shared among the entire hierarchy of the class and we need an exclusive array.
Another fine point is that we force descendants to be the one in the C class. If we didn't and we had A < B < C, the hook would be called when A was defined, but by polymorphism it would be B.descendants what would be called, thus setting B's instance variable @descendants. That is not what we want.
The call to super is just a best practice. In general a hook like this should pass the call up the hierarchy in case parents have their own hooks.
That pattern can be implemented in a module for reuse indeed:
module DescendantsTracker def self.included(base) (class << base; self; end).class_eval do define_method(:inherited) do |subclass| base.descendants << subclass super end end base.extend self end def descendants @descendants ||= [] end end class C include DescendantsTracker end
A class only needs to include DescendantsTracker to track its descendants.
When the module is included in a class Ruby invokes its inherited hook. The hook receives the class that is including the module, and we leverage that to inject the class methods we saw before. For inherited we open the metaclass of base and define the method in a way that has base in scope, which is something we saw before we need. After that we add the descendants class method with an ordinary extend call.
Update: There's a followup to this post.