inherited, ObjectSpace, and Test::Unit

As a part of working on a submission for the homework assignment for Ruby magazine The Rubyist, I wrote a game server so my co-workers and I could test our bots against each other.

Part of what’s needed for a good server is the ability to automatically load classes and instantiate them. If you’re familiar with ObjectSpace and you have a way to identify the bot classes, it’s not too hard. The only thing that identifies a bot class is a move! instance method, but that should be enough:

player_classes = []
ObjectSpace.each_object(Class) do |klass|
  player_classes << klass if klass.instance_methods.include?('move!')
end
player_classes

It’s a little nicer in Ruby 1.9, because if you don’t pass a block to ObjectSpace.each_object, you get an Enumerable::Enumerator, so you can do something like this:

ObjectSpace.each_object(Class).select do |klass|
  klass.instance_methods.include?(:move!)
end

(Ruby 1.9 uses symbols for method names here instead of strings.)

Of course, if you knew that all the player classes would be subclasses of, say, TreasuredRuby::Bot, you could forgo using ObjectSpace altogether and use Class#inherited instead:

class TreasuredRuby::Bot
  PLAYER_CLASSES = []
  def inherited(klass)
    PLAYER_CLASSES << klass
  end
end

Where else are descendant classes treated specially? Test::Unit, specifically Test::Unit::TestCase subclasses. However, Test::Unit uses ObjectSpace to collect its descendant classes. There have been discussions about the problems it creates for JRuby, although JRuby handles ObjectSpace.each_object(Class) because of Test::Unit.

There was recently a patch suggested on the Ruby Core mailing list to replace using ObjectSpace with Class#inherited (and perhaps not the first), but it’s still unapplied. There are arguments against using Class#inherited: if you want to use Class#inherited in your TestCase classes, for example, you had better call super.

We’ll see if Test::Unit changes.

Leave a Comment