ActiveRecord Inheritance: A Maze Of Confusion

Panda Strike's about devops, CoffeeScript, REST, and Node, but we have a lot of recovering Rubyists. Experience in the Rails stack — or really any stack — will teach you that the tech industry sometimes acts more like a pop culture than an engineering one. Keeping your terminology clear of cognitive dissonance can thus become one of the major challenges of developing software.

Here's an example.

A few years ago, I volunteered with a workshop teaching women how to write Rails apps. On the whole, it was a very positive experience. But it made me notice a ton of contradictions within Rails.

Here's a classic way to introduce object-oriented programming to newbiesStrictly speaking, this is much better for teaching about classes than objects. A great way to teach newbies about objects in Ruby is to show them that you can do class << self with almost anything else in the place of self. While this distinction might be esoteric in Ruby, it makes a huge difference in JavaScript.:

class Animal
end

class Dog < Animal; end
class Cat < Animal; end

You might say every animal makes sounds, but different animals make different sounds.

class Dog
  def make_sound
    "woof!"
  end
end

class Cat
  def make_sound
    "meow."
  end
end

And you can say that while a regular Animal moves by walking, a Snake will slither, a Fish will swim, and a FishFromOuterSpace will swim(through: 'space')Or indeed spend all its time identifying traps.
.

class Animal
  def move
    walk
  end
end

class Snake < Animal
  def move
    slither
  end
end

class Fish < Animal
  def move
    swim
  end
end

class FishFromOuterSpace < Fish
  def move
    swim(through: "space")
  end

  def trap?(anything)
    true
  end
end

Now we can do this:

@ackbar = FishFromOuterSpace.new
@ackbar.trap?(it) # => true

So if you explain this to a room full of people who came to learn about object-oriented programming, you're going to do fine. It's a classic teaching example, because it's easy to understand. But you'll run into problems if you next attempt to explain ActiveRecord. In fact, you're likely to answer puzzled questions from the audience by saying things like, "you're right, that doesn't make sense."

Imagine for the sake of argument that you want to track Fish, and FishFromOuterSpace, in a database, so that you can do a statistical comparison of their trap-detecting skills. Assume further that you want to build a web front end to that database. Maybe you want some simple social features, so your Fish can befriend each other and upvote each other's nonsense. That sounds like a reasonable use case for Rails and ActiveRecord. So you might do this:

class Fish < ActiveRecord::Base
  def move
    swim
  end
end

class FishFromOuterSpace < Fish
  def move
    swim(through: "space")
  end

  def trap?(anything)
    true
  end
end

You might even think you could then do this:

Fish.all.each do |fish|
  fish.swim
end

But you would be wrongFor multiple reasons..

For the purposes of this discussion, here's the main problem with that code: none of your fishes will swim through space. The Fish will swim, and the FishFromOuterSpace will swim — but not through space.

Why Are My Space Fish Not Swimming Through Space?

This is a question that every Rails developer asks themselves at some point in their career. To understand the answer, you have to ignore some fundamentals of object-oriented programming, and understand important implementation details behind ActiveRecord.

Active Record is a design pattern; ActiveRecord took its name from the pattern. Martin Fowler first identified the pattern in his book Patterns of Enterprise Application Architecture, where he also said:

Relational databases don't support inheritance, so when mapping from objects to databases we have to consider how to represent our nice inheritance structures in relational tables... Single Table Inheritance maps all fields of all classes of an inheritance structure into a single table.

In practical terms, this means an ActiveRecord model will be backed with a database table, and any of that model's subclasses will be backed with the same database table, which will also feature a column to identify the class. (This column will be named type, not class.)

So, since every FishFromOuterSpace represents data which lives in a table called fish, a method which retrieves this data and reifies it into objects, but which does not look at the table's type column, will assume every FishFromOuterSpace is a Fish (which is true) and only a Fish (which is false).

If you tell Rails to go get you a fish from outer space, it will:

FishFromOuterSpace.first

But you get no guarantees when doing this:

Fish.first

It won't check to see that its Fish are regular fish, not FishFromOuterSpace. You have to tell it what to expect. Which defeats one of the major purposes of inheritance.

ActiveRecord does provide a becomes method, which you can cram into an after_initialize callback, in order to force ActiveRecord to behave like any other object-oriented software:

class Fish < ActiveRecord::Base
  after_initialize :becomes_space_fish_if_not_regular_fish

  def becomes_space_fish_if_not_regular_fish
    becomes self.type if "Fish" != self.type
  end
end

Note that it's self.type, not self.class, because Rails creates these objects as instances of Fish, irrespective of whether they have "Fish" or "FishFromOuterSpace" as the value of their type column. So fish.class would always return Fish in this context:

Fish.all.each do |fish|
  fish.class
end

But this means you could replace "Fish" with self.class.to_s, and alias type to klass — which would be more in keeping with Ruby idioms — if you wanted to write the most awesome code of your entire life:

class Fish < ActiveRecord::Base
  after_initialize :becomes_space_fish_if_not_regular_fish

  alias :klass :type

  def becomes_space_fish_if_not_regular_fish
    becomes klass if self.class.to_s != klass
  end
end

And of course, if you wanted to generalize this, the only thing you would have to change would be the method's name:

class ObjectWhichSupportsInheritanceAfterAll < ActiveRecord::Base
  after_initialize :becomes_other_thing_if_really_already_that_other_thing

  alias :klass :type

  def becomes_other_thing_if_really_already_that_other_thing
    becomes klass if self.class.to_s != klass
  end
end

As you can see, Ruby metaprogramming is so awesome that it allows you to manually rebuild an ad hoc inheritance mechanism for object-oriented classes which do not support inheritance. We can certainly admire Ruby's flexibility in supporting this unusual use case with such grace. And the becomes method certainly comes in handy here.

But you might wonder how Rails was ever able to give us object-oriented classes which do not support inheritance in the first placeTechnically, it doesn't. Technically, it gives us aggregator methods which make inaccurate assumptions about intended data types. But the practical result is very similar to object-oriented classes which do not support inheritance, and for a newbie, it might as well be the same thing.. And you might not be surprised to learn that Rails's single-table inheritance has been confusing Rails newbies since 2004.

As weird as this is, though, it's nothing compared to the weirdness of something every Rails developer does in every Rails app: subclassing ActiveRecord::Base.

ActiveRecord Turns Ruby's Inheritance Mechanism Into An Abstract Factory

Every Ruby object has an #inherited method. To quote ruby-doc.com:

This is a singleton method (per class) invoked by Ruby when a subclass... is created. The new subclass is passed as a parameter.

Most Ruby software leaves this method unmodified, but ActiveRecord::Base implements it. Here's what the implementation looks like:

def inherited(child_class) #:nodoc:
  child_class.initialize_generated_modules
  super
end

It gets pretty involved from there, and parts of this custom inheritance mechanism are distributed throughout two gems — ActiveRecord and ActiveModel — but here's the basic idea. The mechanism grabs methods from modules throughout both ActiveRecord and ActiveModel, and returns a new class with these methods attached.

Compare this to the Wikipedia definition for the Abstract Factory design pattern:

The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes.

This is what ActiveRecord does. It creates a new class through the usual inheritance mechanism. Then it encapsulates a group of individual subsystems, without specifying them externally. Those systems do the work of factories, putting new methods onto the class, building it. The only difference between ActiveRecord and the Wikipedia definition of an Abstract Factory is that ActiveRecord keeps its individual factories in concrete modules, not concrete classes.

A classic Abstract Factory implementation would call Class.new, attach methods from elsewhere, and return a class, so you could then provide your own custom functionality for it. Rails does all of this, except instead of using Class.new, it embeds the whole process inside Ruby's inheritance machinery (which obviously also returns a new class).

But imagine an alternate universe where ActiveRecord behaved like Struct, from Ruby's standard library:

Fish = ActiveRecord.new do
  def move
    swim
  end
end

class FishFromOuterSpace < Fish
  def move
    swim(through: "space")
  end

  def trap?(anything)
    true
  end
end

I think this code's easier to readAfter all, Base is not the most descriptive or self-documenting name ever featured in an object-oriented system. It's not as if any of the gazillion ActiveRecord::Base subclasses out there represent some more specific kind of Base.. It would not necessarily get us all the way back to object-oriented classes which supported inheritance, but it'd certainly make the route easier to reason about.

The Valley Finds Its Own Uses For Words

The irony of this torturous, convoluted story is that Rails's Abstract Factory implementation is one of the most popular, successful, and productive Abstract Factory implementations in the history of software, despite being arguably also the most demented and stringly-typedThe next level of irony, of course, is that the Rails culture's often heaped contempt on the whole concept of design patterns, even though ActiveRecord implements the Active Record design pattern deliberately, and the Abstract Factory pattern accidentally..

This illustrates the fundamental tension between pedantic correctness and reckless redefinition. The tech industry mostly speaks a kind of engineering slang, where all the terminology has precise roots and imprecise usage. To build your first Rails app, you have to go with the flow of the framework; to debug weird Rails issues, you have to understand not just that it has bugs in execution, but also that it embeds misunderstandings deep within its internals.

In other words, the following line of code has both a real meaning and a slang meaning:

class Fish < ActiveRecord::Base

And if you want to work well with that line of code, you should know both its slang meaning and its real meaning.

This weird dichotomy is not unique to Rails at all. In fact, it's not even unique to writing code.

To quote Alan Kay:

Once you have something that grows faster than education grows, you’re always going to get a pop culture.

Kay's primary analogy here is pop music, and the analogy holds. To name just two pop stars who fit this pattern, Nicki Minaj studied music theory, and Tupac Shakur studied balletAlso, this is completely irrelevant, but Queen guitarist Brian May has a doctorate in astrophysics.. But this is not such a surprise when you think about it from this point of view.

It would be ridiculous to assume a similar educational background for every single musician Nicki Minaj or Tupac ever collaborated with. But it's likewise ridiculous to assume that every programmer you talk to is using words they fully understand.

In either case, with both programming and music, you've got a shallow pop culture built on a complex, sophisticated foundation. In the same way that understanding classical theory will help you craft pop hits, a good programmer should not just know how to grab stuff off GitHub and get it working right away, but also understand classic ideas of computer science. You should be able to approach problems from either perspective, and to reconcile the inevitable conflicts between them.

Notes