Taking Advantage of Rails

Photo From Flickr: http://bit.ly/2abv0Ms

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.

  1. ActiveSupport::Dependencies has an array that keeps track of all the autoloaded constants. The .autoloaded? method simply checks for inclusion in that array.
Eryan is a development director in DevMynd’s software engineering practice focusing on mobile apps and web development. He has been with the company since 2013.