The Problem
While working on a Rails project recently, I had a problem that I’m sure many people can relate to—I had a <select>
tag in my view, and I needed customers to choose from several options that directly corresponded to some of the models in my application. Fortunately the models were well-named, so I could use a slightly modified versions of the class name for each <option>
tag. All of these models were subclasses of one parent class, and some had their own descendant classes as well (which I did not need to include in the <select>
). Finally, none of these models needed to persist to the database.
There are a few ways to handle something like this. You could hardcode all of the <option>
tags in the view (for the sake of everyone touching that code in the future, please don’t do this). Or, slightly better, you could create a constant in the parent class, either an array with all of the names you want to display, or maybe a hash that matches the display name to its corresponding class name. The problem with both of these approaches is that it is up to people maintaining the application to keep that list up to date—eventually someone is going to forget. There’s a better way to solve this problem.
Sometimes in the course of building a Rails app, you need to create your own Plain Old Ruby Objects (PORO). But, just because you need to do that doesn’t mean you’re left out in the cold, without the many helpful methods and functionality that Rails provides automatically. You can still take advantage of what Rails offers by including or extending many of the same modules that comprise Rails. All that code is just sitting there, waiting for you to tap into it.
It’s generally a good idea to understand a little bit about including vs extending modules when you’re doing this, so if you need to brush up on that or other concepts we might touch on at some point, such as singleton classes (eigenclasses, if you prefer), or metaprogramming, I would highly recommend checking out the excellent Eloquent Ruby book. It’s a little bit outdated at this point, since it was written when Ruby 1.9.3 was still recent, but most of the information is still accurate. You don’t necessarily need that knowledge to use the modules, but it helps to understand what’s going on, and this blog post isn’t going anywhere.
ActiveSupport::DescendantsTracker
If you’ve already read Eloquent Ruby, then you know that it’s possible to track all of the descendants of a Ruby class by defining a class method self.inherited(subclass)
and storing the subclass names that get passed in each time a new subclass is defined. However, Rails offers you another way to get that same functionality: by extending the ActiveSupport::DescendantsTracker
module.
If you do a brief search through the Rails source code, you’ll see several other modules that extend ActiveSupport::DescendantsTracker
, such as ActiveRecord::Base
, ActionMailer::Preview
, and AbstractController::Base
. Feel free to take a moment and call ActiveRecord::Base.direct_descendants
in the console of one of your projects. You should see some of your own models in that list.
So, why would you bother extending this module rather than just implementing the self.inherited
method yourself? Well, the main reason is avoiding the hassle of writing, testing, and maintaining unnecessary and duplicative code. Another is that it may cover some things you may not have thought of in your implementation. For example, this would be a basic implementation of descendant tracking:
class Foo class « self attr_reader :subclasses end @subclasses = [] def self.inherited(new_subclass) Foo.subclasses « new_subclass end end class Bar < Foo end class Baz < Bar end Foo.subclasses => [Bar, Baz]
One small improvement that DescendantsTracker
makes over this basic implementation is its .direct_descendants
method, which allows you to differentiate between the direct descendants of a class, and all of the descendants of a class. So, you can do this instead:
class Foo extend ActiveSupport::DescendantsTracker end class Bar < Foo end class Baz < Bar end Foo.descendants => [Bar, Baz] Foo.direct_descendants => [Bar]
It saves you a little work and adds some of functionality. Not so bad. It was exactly what I needed, since there were other descendants of the base class (grandchildren, basically) that I didn’t need to display.
Internals
It’s always a good idea to dig around in the Rails source code when you get the chance. Many times you end up discovering methods you didn’t know existed, or learning a new technique of dealing with a problem. With that being said, lets take a closer look at the DescendantsTracker
module.
It starts off by adding a hash as a class variable to store all of the constants
@@direct_descendants = {}
Then it uses the singleton class to add a few class methods and a private method to ActiveSupport::DescendantsTracker
. We can look at each of these individually.
def direct_descendants(klass) @@direct_descendants[klass] || [] end
Pretty straightforward here. It just uses the @@direct_descendants
hash to return whatever array of direct descendants exists for the class that gets passed in, or an empty array if that class has no direct descendants.
def descendants(klass) arr = [] accumulate_descendants(klass, arr) arr end
This is slightly more interesting. That hash only stores direct descendants, so in order to pick up all of a class’ descendants, it calls this .accumulate_descendants
private method and passes it an empty array to hold everything it finds, then it returns the array. We’ll cover the .accumulate_descendants
method shortly.
def clear if defined? ActiveSupport::Dependencies @@direct_descendants.each do |klass, descendants| if ActiveSupport::Dependencies.autoloaded?(klass) @@direct_descendants.delete(klass) else descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) } end end else @@direct_descendants.clear end end
This .clear
method first checks to see if ActiveSupport::Dependencies
is defined, since it needs to use a method from that module. If it isn’t defined, then the direct_descendants hash just gets cleared out. If it is defined, then it iterates through each key (classes that have direct descendants) and checks whether that class has been autoloaded to decide whether it should be deleted outright,1 or whether it should stay (if it stays, then any descendants that have been autoloaded will be deleted). The .clear
method is used in development in order to quickly reload the environment.
def store_inherited(klass, descendant) (@@direct_descendants[klass] ||= []) << descendant end
The .store_inherited
method is also straightforward. It takes the parent class and adds it as a key in the @@direct_descendants
hash if it isn’t already there, then it adds the descendant to the array for the parent class.
def accumulate_descendants(klass, acc) if direct_descendants = @@direct_descendants[klass] acc.concat(direct_descendants) direct_descendants.each { |direct_descendant| accumulate_descendants(direct_descendant, acc) } end end
This is the private method that makes the .descendants
call work. It adds all of the direct descendants of the current class (conveniently stored in the @@direct_descendants
hash) to an accumulator array, and then recursively calls itself on all of those descendants also passing in the accumulator.
Gotchas
The first time I tried to use the .direct_descendants
method and view the page with the <select>
tag that I wanted people to pick from, things didn’t really go as planned. Instead of seeing a bunch of options, like I expected, I kept getting an empty select. I tried to do some of the usual debugging methods (putting in a pry, testing things out in the console), but didn’t really understand what was happening. The method just wasn’t working for me. Even more frustrating, I deployed to staging to see what would happen, and it worked perfectly there. It was only once I tried out a couple of the subclasses in the rails console on development and saw that only the ones I had tried out appeared on the page that the lightbulb appeared above my head with the answer: autoloading.
One thing you need to watch out for in the development environment when using the DescendantsTracker module is that because of the way autoloading and eager loading work in Rails you will have to make sure each one of the descendants of a class is loaded before you can get an accurate list when calling .descendants
. For example lets say you have these three models, each in its respective file:
# foo.rb class Foo extend ActiveSupport::DescendantsTracker end
# bar.rb class Bar < Foo end
# baz.rb class Baz < Bar end
If you boot up a rails console, the following things would happen:
Foo.descendants => [] Bar => Bar Foo.descendants => [Bar] Baz => Baz Foo.descendants => [Bar, Baz]
As you can see, due to the way autoloading works in the dev environment, the Bar
and Baz
classes aren’t loaded until they’re needed. If you had called Baz
first, then both Bar
and Foo
would have been autoloaded. There are several ways you can fix modify this behavior, such as turning on eager loading in dev or adding an initializer that will take care of loading all the files you need. Turning on eager loading isn’t an awful solution, but it’s off by default in dev in order to speed up boot times. That may not matter as much at the start of a project, but its importance usually increases as your codebase and dependencies grow. In most cases it’s much more reasonable to just add an initializer that loads the classes you need to keep track of.
This isn’t an issue in production, since all of the files in the app/ directory are eager loaded by default.
Conclusion
ActiveSupport::DescendantsTracker
is one of the many useful Rails modules that you can use in your own classes. It helped me solve my issue, and hopefully it can help with one of yours as well. I’d like to go through other useful Rails modules in this series and talk a little more about how they can simplify or improve your code, and save you from having to jump through hurdles to replicate functionality that already exists. Are you already including some modules in your applications? Send a message with one of your favorites and I’ll try to cover it.
ActiveSupport::Dependencies
has an array that keeps track of all the autoloaded constants. The.autoloaded?
method simply checks for inclusion in that array. ↩