Deep Rails: How to use Abstract Classes

Occasionally in your travels, you’ll come across a little class-level declaration that sets a class as abstract:

class SomeAbstractModel < ApplicationRecord
self.abstract_class = true

# some heritable methods here
end

This magic line has been the solution to some wandering Rubyists’ inheritence problems, and the source of many more. How does it work? What should we use it for? Come on: let’s check it out together.

First Things First: What Does it Do?

Put simply: when Rails creates a model as an abstract class it does so without an underlying table. That means that this model can never be instantiated. Ever! Under any circumstances. Doing so throws an error:

SomeAbstractClass.create!# => NotImplementedError: (Vehicle is an abstract class and cannot be instantiated.)

An abstract class is created with all of the methods you’d expect to find on ApplicationRecord — validators, persistence methods, etc — but an instance of an abstract class can never exist on its own.

So Then Why Bother Using Them?

Single-Table Inheritance (STI) in Rails is famously problematic, and abstract classes — when wielded appropriately — offer a solution to one of the traits of STI that developers are most frustrated by.

When you create a heritable model in Ruby, STI dictates that that model has a :type column. That column (as expected) stores the type of the object. STI also dictates that this parent model has columns for any and every column that a child record might need.

Consider, for example, a class called Vehicle:

class Vehicle < ApplicationRecord
with_options presence: true, allow_blank: false do
validates :weight
validates :color
end
def convert_weight(unit)
case unit
when :lbs
weight * 2.20462
when :g
weight * 1000.0
end
end
end

and two children of Vehicle:

class Car < Vehicle
validates :number_of_wheels, presence: true, allow_blank: false
end
class Airplane < Vehicle
validates :number_of_wings, presence: true, allow_blank: false
end

Within the STI paradigm, the migration for Vehicle would create a table that has a :type column. If the created object is a car, then the type will be "Car”. If the created object is an Airplane, then the type will be "Airplane”. That’s all well and good.

But the vehicles table would also need a :number_of_wheels column and a :number_of_wings column, because those attributes are required by some of its child records:

create_table :vehicles do |t|
# columns required by some children
t.integer :number_of_wheels
t.integer :number_of_wings
# columns required by all children
with_options null: false do
t.string :type # required for inheritence
t.integer :weight
t.string :color
end
end

Now take a second to unravel this thread: as we plan out more of these models’ behavior we’ll realize that a Car and an Airplane are very different vehicles. The attributes that they don’t share might number in the hundreds. That’s a lot of columns on this shared table that will sit empty.

And what about when we create Boat, Snowmobile, and Toboggan? It seems like we’re headed for one big, bloated table. This is one of developers deepest, darkest grievances with Rails’ inheritance mechanism.

Abstract Classes to the Rescue!

Abstract classes allow for something that resembles a true interface object in Rails: the model produces holds behavior that’s common to all of its children, but — because it has no data representation — it holds (and knows nothing of) the data required by its children.

Let’s reconsider our class Vehicle as an abstract class:

class Vehicle < ApplicationRecord
self.abstract_class = true
with_options presence: true, allow_blank: false do
validates :weight
validates :color
end
def convert_weight(unit)
case unit
when :lbs
weight * 2.20462
when :g
weight * 1000.0
end
end
end

And the migration for Vehicle:

# *chirp* *chirp*

Get it? There’s no data representation whatsoever. The data representation is entirely defined by the classes that inherit from Vehicle:

class CreateVehicles < ActiveRecord::Migration[5.2]
def change
create_table :cars do |t|
with_options null: false do
t.integer :weight
t.string :color
t.integer :number_of_wheels
end
end
create_table :airplanes do |t|
with_options null: false do
t.integer :weight
t.string :color
t.integer :number_of_wings
end
end
end
end

In both tables, we’ll expect to see 100% utilization on every column. We’ll have specialized tables for the objects that they represent, but still get to share our behavior across models.

Wrap Up: When Should I use Abstract Classes?

This is not better than STI, per se, but it may be a better solution in some cases, particularly when child models may have a very different set of characteristics from one another.

This is one technique of many for creating well-organized Rails models, and it’s by no means universally applicable. There will be times when you’d rather compose models using Ruby modules and extend, prepend, and include. And there will absolutely be times where standard Single-Table Inheritence is your easiest and best solution for creating heritable behaviors.

Understanding what these techniques do is key to understanding how and when they should be used. Context and constraint is everything in a framework as flexible as Rails. Understand what you need, then choose the pattern that will serve you best.

Rubyist, Writer, Musician, Educator, Bread Enthusiast

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store