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
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
swim, and a
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
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
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
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.
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
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
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:
But you get no guarantees when doing this:
It won't check to see that its
Fish are regular fish, not
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.class, because Rails creates these objects as instances of
Fish, irrespective of whether they have
"FishFromOuterSpace" as the value of their
fish.class would always return
Fish in this context:
Fish.all.each do |fish| fish.class end
But this means you could replace
self.class.to_s, and alias
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.
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:
ActiveRecord Turns Ruby's Inheritance Mechanism Into An Abstract Factory
Every Ruby object has an
#inherited method. To quote
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 —
ActiveModel — but here's the basic idea.
The mechanism grabs methods from modules throughout both
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
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
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.